From 121870411affebf59c9434844642b942d3f419ae Mon Sep 17 00:00:00 2001 From: rain Date: Mon, 15 Nov 2021 22:45:56 +0000 Subject: [PATCH 1/8] hevm abiencode: support tuple types --- src/dapp-tests/integration/tests.sh | 57 +++++++++++++++++++++++++++++ src/hevm/src/EVM/ABI.hs | 22 ++++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/dapp-tests/integration/tests.sh b/src/dapp-tests/integration/tests.sh index 84b026bf2..16fa69432 100755 --- a/src/dapp-tests/integration/tests.sh +++ b/src/dapp-tests/integration/tests.sh @@ -463,6 +463,63 @@ test_calldata_19() { } test_calldata_20() { + local output + output=$(seth calldata 'f(tuple(bool))' "(true)") + + assert_equals "0x19cabbc50000000000000000000000000000000000000000000000000000000000000001" "$output" +} + +test_calldata_21() { + local output + output=$(seth calldata 'f((bool, bool))' "(true, false)") + + assert_equals "0x59c4785b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000" "$output" +} + +test_calldata_22() { + local output + output=$(seth calldata 'f(Data(bool, bool))' "(true, false)") + + assert_equals "0x59c4785b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000" "$output" +} + +test_calldata_23() { + local output + output=$(seth calldata 'f(uint, Data(bool, bytes))' 1 "(true, 0xcafe)") + + assert_equals "0xf1dd2cfe00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002cafe000000000000000000000000000000000000000000000000000000000000" "$output" +} + +test_calldata_24() { + local output + output=$(seth calldata "_f2(address, bool, uint32, address,(bool[] x,uint256, uint32[4] y), bytes memory b) public view mod returns (bool,(bytes,string)) ; " \ + 0x49c92F2cE8F876b070b114a6B2F8A60b83c281Ad true 111 0x49c92F2cE8F876b070b114a6B2F8A60b83c281Ad "([true, false], 33, [1, 2, 3, 4])" 0xcafefe) + + assert_equals "0x3c31a0e800000000000000000000000049c92f2ce8f876b070b114a6b2f8a60b83c281ad0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006f00000000000000000000000049c92f2ce8f876b070b114a6b2f8a60b83c281ad00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003cafefe0000000000000000000000000000000000000000000000000000000000" "$output" +} + +test_calldata_25() { + local output + output=$(seth calldata 'f(uint[][], Data(bool, bytes)[][2])' "[[1, 2], [3]]" "[[(true, 0xaa), (true, 0xbb)], [(true, 0xcc)]]") + + assert_equals "0x6ba52bdd000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001aa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001bb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001cc00000000000000000000000000000000000000000000000000000000000000" "$output" +} + +test_calldata_26() { + local output + output=$(seth calldata 'f((uint, uint)[][3])' "[[(10, 11), (12, 13)], [(14, 15), (16, 17)], [(18, 19), (20, 21)]]") + + assert_equals "0x86c6d56a00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000d0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000f0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001300000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000015" "$output" +} + +test_calldata_27() { + local output + output=$(seth calldata 'f((uint, uint)[2][3])' "[[(10, 11), (12, 13)], [(14, 15), (16, 17)], [(18, 19), (20, 21)]]") + + assert_equals "0x4c1620bd000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000f000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001300000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000015" "$output" +} + +test_calldata_28() { local output output=$(seth calldata 'foo(bytes32, bytes4, bytes16)' '0x' '0x' '0x') diff --git a/src/hevm/src/EVM/ABI.hs b/src/hevm/src/EVM/ABI.hs index b459b2073..a06f3a69b 100644 --- a/src/hevm/src/EVM/ABI.hs +++ b/src/hevm/src/EVM/ABI.hs @@ -512,7 +512,27 @@ parseAbiValue (AbiArrayDynamicType typ) = parseAbiValue (AbiArrayType n typ) = AbiArray n typ <$> do a <- listP (parseAbiValue typ) return $ Vector.fromList a -parseAbiValue (AbiTupleType _) = error "tuple types not supported" +parseAbiValue (AbiTupleType types) = + AbiTuple <$> do args <- tupleP [parseAbiValue typ | typ <- Vector.toList types] + return $ Vector.fromList args + +tupleP :: [ReadP a] -> ReadP [a] +tupleP parsers = between (char '(') (char ')') (argP parsers) + +argP :: [ReadP a] -> ReadP [a] +argP [] = do return [] +argP (p:[]) = do + skipSpaces + arg <- p + skipSpaces + return [arg] +argP (p:ps) = do + skipSpaces + arg <- p + skipSpaces + char ',' + args <- argP ps + return (arg:args) listP :: ReadP a -> ReadP [a] listP parser = between (char '[') (char ']') ((do skipSpaces From 64d24c235877b4fbb50e3e85da03ca2abe0a03f9 Mon Sep 17 00:00:00 2001 From: rain Date: Mon, 22 Nov 2021 00:27:50 +0000 Subject: [PATCH 2/8] hevm: abiencode solidity fragments `hevm abiencode` now accepts solidity interface definitions: hevm abiencode --abi "f(bool b, uint x, Data(bytes[], string))" New commands `hevm normalise` and `hevm selector` to give the canonical signature and signature hash for a given --abi. --- src/hevm/hevm-cli/hevm-cli.hs | 52 +++++++++++-- src/hevm/hevm.cabal | 3 +- src/hevm/shell.nix | 7 +- src/hevm/src/EVM/SolidityABI.hs | 98 +++++++++++++++++++++++++ src/seth/libexec/seth/seth---abi-encode | 22 ++++++ src/seth/libexec/seth/seth-abi-encode | 12 --- src/seth/libexec/seth/seth-calldata | 17 +---- src/seth/libexec/seth/seth-index | 3 +- src/seth/libexec/seth/seth-sig | 26 +------ 9 files changed, 177 insertions(+), 63 deletions(-) create mode 100644 src/hevm/src/EVM/SolidityABI.hs create mode 100755 src/seth/libexec/seth/seth---abi-encode delete mode 100755 src/seth/libexec/seth/seth-abi-encode diff --git a/src/hevm/hevm-cli/hevm-cli.hs b/src/hevm/hevm-cli/hevm-cli.hs index 6fdd91e72..bbdd09925 100644 --- a/src/hevm/hevm-cli/hevm-cli.hs +++ b/src/hevm/hevm-cli/hevm-cli.hs @@ -34,6 +34,7 @@ import EVM.SymExec import EVM.Debug import EVM.ABI import EVM.Solidity +import EVM.SolidityABI import EVM.Types hiding (word) import EVM.UnitTest (UnitTestOptions, coverageReport, coverageForUnitTestContract) import EVM.UnitTest (runUnitTestContract) @@ -228,6 +229,16 @@ data Command w { abi :: w ::: Maybe String "Signature of types to decode / encode" , arg :: w ::: [String] "Values to encode" } + | Abidecode + { abi :: w ::: Maybe String "Signature of types to decode / encode" + , calldata :: w ::: Maybe ByteString "Tx: calldata" + } + | Normalise + { abi :: w ::: Maybe String "Signature of types to decode / encode" + } + | Selector + { abi :: w ::: Maybe String "Signature of types to decode / encode" + } | MerkleTest -- Insert a set of key values and check against the given root { file :: w ::: String "Path to .json test file" } @@ -328,6 +339,12 @@ main = do launchExec cmd Abiencode {} -> print . ByteStringS $ abiencode (abi cmd) (arg cmd) + Abidecode {} -> + print $ abidecode (abi cmd) (calldata cmd) + Normalise {} -> + print $ normaliseSig (abi cmd) + Selector {} -> + print . ByteStringS $ abiselector (abi cmd) BcTest {} -> launchTest cmd DappTest {} -> @@ -968,14 +985,35 @@ runVMTest diffmode mode timelimit (name, x) = #endif -parseAbi :: (AsValue s) => s -> (Text, [AbiType]) -parseAbi abijson = - (signature abijson, snd - <$> parseMethodInput - <$> V.toList - (fromMaybe (error "Malformed function abi") (abijson ^? key "inputs" . _Array))) -abiencode :: (AsValue s) => Maybe s -> [String] -> ByteString +normaliseSig :: Maybe String -> Text +normaliseSig Nothing = error "missing required argument: abi" +normaliseSig (Just abi) = fst $ parseAbi abi + +abidecode :: Maybe String -> Maybe ByteString -> String +abidecode Nothing _ = error "missing required argument: abi" +abidecode (Just abi) (Just cd) = + let (_, types) = parseAbi abi + values = decodeAbiValue (AbiTupleType (V.fromList types)) (Lazy.fromStrict cd) + in show values + +abiselector :: Maybe String -> ByteString +abiselector Nothing = error "missing required argument: abi" +abiselector (Just abi) = selector $ fst $ parseAbi abi + +parseAbi :: String -> (Text, [AbiType]) +parseAbi abi = + case (abi ^? key "inputs" . _Array) of + Just inputs -> + (signature abi, snd <$> parseMethodInput <$> V.toList inputs) + Nothing -> + case parseSolidityAbi abi of + Left e -> error $ "could not parse abi fragment. result: " ++ e + Right (FunctionFragment name types _ _) -> + let sig = pack $ name <> (show (AbiTupleType (V.fromList types))) + in (sig, types) + +abiencode :: Maybe String -> [String] -> ByteString abiencode Nothing _ = error "missing required argument: abi" abiencode (Just abijson) args = let (sig', declarations) = parseAbi abijson diff --git a/src/hevm/hevm.cabal b/src/hevm/hevm.cabal index cf36961e6..50d3d017f 100644 --- a/src/hevm/hevm.cabal +++ b/src/hevm/hevm.cabal @@ -2,7 +2,7 @@ cabal-version: 2.2 name: hevm version: - 0.49.0 + 0.50.0 synopsis: Ethereum virtual machine evaluator description: @@ -53,6 +53,7 @@ library EVM.Precompiled, EVM.RLP, EVM.Solidity, + EVM.SolidityABI, EVM.Stepper, EVM.StorageLayout, EVM.Symbolic, diff --git a/src/hevm/shell.nix b/src/hevm/shell.nix index d3ed04b0c..bc80d5bd0 100644 --- a/src/hevm/shell.nix +++ b/src/hevm/shell.nix @@ -2,14 +2,17 @@ let inherit (dapphub) pkgs; - drv = pkgs.haskellPackages.shellFor { packages = p: [ p.hevm ]; - buildInputs = with pkgs.haskellPackages; [ + buildInputs = with pkgs.haskellPackages; with pkgs; [ + ghci + ghcid cabal-install haskell-language-server + bc coreutils curl ethsign git gnused nix jq hevm jshon nodejs tre perl solc + gnugrep ]; withHoogle = true; }; diff --git a/src/hevm/src/EVM/SolidityABI.hs b/src/hevm/src/EVM/SolidityABI.hs new file mode 100644 index 000000000..6c8f8e549 --- /dev/null +++ b/src/hevm/src/EVM/SolidityABI.hs @@ -0,0 +1,98 @@ +module EVM.SolidityABI ( parseSolidityAbi , Fragment (..) ) where + +import EVM.ABI + +import Control.Monad (when) +import Data.Char (isDigit, isAlphaNum, isAlpha) +import Text.ParserCombinators.ReadP + +import qualified Data.Vector as V + +data Fragment = FunctionFragment + { _name :: String + , _types :: [AbiType] + , _returns :: [AbiType] + , _modifiers :: [String] + } + deriving Show + +parseSolidityAbi :: String -> Either String Fragment +parseSolidityAbi abi = + case readP_to_S parseFragment abi of + [(val,"")] -> Right val + r -> Left (show r) + +isName1 :: Char -> Bool +isName1 c = isAlpha c || c == '_' + +isName :: Char -> Bool +isName c = isAlphaNum c || c == '_' + +identifier :: ReadP String +identifier = do + c <- satisfy isName1 + s <- munch isName + return (c:s) + +space :: ReadP () +space = skipMany1 (char ' ') + +excluding :: (Eq a) => ReadP a -> [a] -> ReadP a +excluding p x = do + name <- p + when (elem name x) pfail + return name + +parseFragment :: ReadP Fragment +parseFragment = do + optional (skipSpaces *> string "function" *> space) + name <- (skipSpaces *> identifier) + types <- (skipSpaces *> parseTypes) + -- n.b. modifier arguments are not supported + modifiers <- option [] $ + space *> sepBy1 (identifier `excluding` ["returns"]) (many1 (char ' ')) + -- allow us to parse f(inputs)(outputs) with or without the "returns" + returns <- option [] $ + optional (space *> string "returns") *> skipSpaces *> parseTypes + skipSpaces + optional (char ';') + skipSpaces + eof + return (FunctionFragment name types returns modifiers) + +parseTypes :: ReadP [AbiType] +parseTypes = between + (char '(' *> skipSpaces) + (skipSpaces <* char ')') + (sepBy parseType (skipSpaces <* char ',' *> skipSpaces)) + +parseType :: ReadP AbiType +parseType = do + t <- parseBasicType + s <- many (between (char '[') (char ']') (munch isDigit)) + optional (space *> location) + optional (space *> argName) + return (foldl makeArray t s) + where + location = (string "memory" +++ string "calldata") + argName = identifier `excluding` ["memory", "calldata"] + + makeArray :: AbiType -> String -> AbiType + makeArray t "" = AbiArrayDynamicType t + makeArray t s = AbiArrayType (read s) t + + parseBasicType :: ReadP AbiType + parseBasicType = choice + [ AbiBoolType <$ string "bool" + , AbiAddressType <$ string "address" + , AbiStringType <$ string "string" + , AbiBytesDynamicType <$ string "bytes" + , AbiBytesType <$> (string "bytes" *> (read <$> munch1 isDigit)) + , (<++) + (AbiUIntType <$> (string "uint" *> (read <$> munch1 isDigit))) + (AbiUIntType 256 <$ string "uint") + , (<++) + (AbiIntType <$> (string "int" *> (read <$> munch1 isDigit))) + (AbiIntType 256 <$ string "int") + , AbiTupleType <$> V.fromList <$> (optional identifier *> parseTypes) + ] diff --git a/src/seth/libexec/seth/seth---abi-encode b/src/seth/libexec/seth/seth---abi-encode new file mode 100755 index 000000000..87cc49fd8 --- /dev/null +++ b/src/seth/libexec/seth/seth---abi-encode @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +### seth---abi-encode -- ABI encode signature and arguments +### Usage: seth abi-encode [] +set -e +[[ $1 ]] || seth --fail-usage "$0" + +abi="$1" +shift +args=() +for arg; do + [[ $arg = @* ]] && arg=$(seth lookup "$arg") + args+=(--arg "$arg") +done + +calldata=$(hevm abiencode --abi "$abi" "${args[@]}") + +if [[ $? == 1 ]]; then + echo >&2 "hevm: $calldata" + exit 1 +else + echo "0x${calldata#0x}" +fi diff --git a/src/seth/libexec/seth/seth-abi-encode b/src/seth/libexec/seth/seth-abi-encode deleted file mode 100755 index b753de17e..000000000 --- a/src/seth/libexec/seth/seth-abi-encode +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -### seth-abi-encode -- ABI encode values, and prints the encoded values without the function signature -### Usage: seth abi-encode [] -### -### ABI encode values based on a provided function signature, slice off the leading the function signature, -### and print the result. It does not matter what the name of the function is, as only the types and values -### affect the output. - -set -e - -x=$(seth calldata $@); # generate full calldata based on function signature -echo "0x${x:10}" # slice off the function signature and only keep the encoded values diff --git a/src/seth/libexec/seth/seth-calldata b/src/seth/libexec/seth/seth-calldata index 2d625208a..a7c798caf 100755 --- a/src/seth/libexec/seth/seth-calldata +++ b/src/seth/libexec/seth/seth-calldata @@ -11,22 +11,7 @@ ### Otherwise, ensure begins with `0x' and treat it as hexdata. set -e if [[ $1 =~ ^([^\(]+)\( ]]; then - abi=$(seth --abi-function-json "$1") - shift - args=() - for arg; do - [[ $arg = @* ]] && arg=$(seth lookup "$arg") - args+=(--arg "$arg") - done - calldata=$( - hevm abiencode --abi "$abi" "${args[@]}" - ) - if [[ $calldata = error* ]]; then - echo >&2 "hevm: $calldata" - exit 1 - else - echo "0x${calldata#0x}" - fi + seth --abi-encode "$@" elif [[ $2 ]]; then seth --fail-usage "$0" else diff --git a/src/seth/libexec/seth/seth-index b/src/seth/libexec/seth/seth-index index 505549344..bf988ed09 100755 --- a/src/seth/libexec/seth/seth-index +++ b/src/seth/libexec/seth/seth-index @@ -15,7 +15,8 @@ set -e [[ $# -eq 4 ]] || [[ $# -eq 5 ]] || seth --fail-usage "$0" if [[ $5 = 'vyper' || $5 = 'vy' || $5 = 'v' ]]; then - # note: not guaranteed to be accurate for all Vyper versions since storage layout is not yet stable + # note: not guaranteed to be accurate for all Vyper versions since storage + # layout is not yet stable # more info: https://twitter.com/big_tech_sux/status/1420159854170152963 echo >&2 "${0##*/}: warning: not guaranteed to be accurate for all Vyper versions since storage layout is not yet stable" echo $(seth keccak $(seth abi-encode "x($2,$1)" $4 $3)); diff --git a/src/seth/libexec/seth/seth-sig b/src/seth/libexec/seth/seth-sig index 8d4ab663d..6db216379 100755 --- a/src/seth/libexec/seth/seth-sig +++ b/src/seth/libexec/seth/seth-sig @@ -6,27 +6,5 @@ set -e [[ $# = 1 ]] || seth --fail-usage "$0" -# Get ABI from input -abi=$(seth --abi-function-json "$1") -inputs=$(echo "$abi" | jq '.inputs[] | .type') - -# Generate dummy args -args=() -for input in $inputs; do - type="${input//\"}" # remove leading and trailing quotes from the JSON - end=${type: -1} # get the last character to check for array types - if [[ "$end" = "]" ]]; then - arg="[]" - elif [[ "$type" =~ ^byte.*$ ]]; then - arg="0x" - elif [[ "$type" =~ ^string$ ]]; then - arg='""' - else - arg=0 - fi - args+=(--arg "$arg") -done - -# Use dummy args generate calldata and only keep the function selector -calldata=$(hevm abiencode --abi "$abi" "${args[@]}") -echo "${calldata:0:10}" +hash=$(seth keccak "$(hevm normalise --abi "$1")") +echo "${hash:0:10}" From 059c2f5d88e205bb9f9470e66a33dce64208ad76 Mon Sep 17 00:00:00 2001 From: rain Date: Fri, 19 Nov 2021 05:51:47 +0000 Subject: [PATCH 3/8] hevm: abidecode New command `hevm abidecode`, allowing for decoding of e.g. function calldata or returndata, given an ABI. --- src/hevm/hevm-cli/hevm-cli.hs | 50 ++++++++++++++++---- src/seth/libexec/seth/seth---abi-decode | 43 +++-------------- src/seth/libexec/seth/seth---calldata-decode | 33 +++---------- 3 files changed, 54 insertions(+), 72 deletions(-) diff --git a/src/hevm/hevm-cli/hevm-cli.hs b/src/hevm/hevm-cli/hevm-cli.hs index bbdd09925..e39696cea 100644 --- a/src/hevm/hevm-cli/hevm-cli.hs +++ b/src/hevm/hevm-cli/hevm-cli.hs @@ -230,8 +230,9 @@ data Command w , arg :: w ::: [String] "Values to encode" } | Abidecode - { abi :: w ::: Maybe String "Signature of types to decode / encode" - , calldata :: w ::: Maybe ByteString "Tx: calldata" + { abi :: w ::: Maybe String "Signature of types to decode / encode" + , calldata :: w ::: Maybe ByteString "Tx: calldata" + , returndata :: w ::: Maybe ByteString "returndata" } | Normalise { abi :: w ::: Maybe String "Signature of types to decode / encode" @@ -340,7 +341,7 @@ main = do Abiencode {} -> print . ByteStringS $ abiencode (abi cmd) (arg cmd) Abidecode {} -> - print $ abidecode (abi cmd) (calldata cmd) + print $ abidecode (abi cmd) (calldata cmd) (returndata cmd) Normalise {} -> print $ normaliseSig (abi cmd) Selector {} -> @@ -990,13 +991,34 @@ normaliseSig :: Maybe String -> Text normaliseSig Nothing = error "missing required argument: abi" normaliseSig (Just abi) = fst $ parseAbi abi -abidecode :: Maybe String -> Maybe ByteString -> String -abidecode Nothing _ = error "missing required argument: abi" -abidecode (Just abi) (Just cd) = - let (_, types) = parseAbi abi - values = decodeAbiValue (AbiTupleType (V.fromList types)) (Lazy.fromStrict cd) +abidecode :: Maybe String -> Maybe ByteString -> Maybe ByteString -> String +abidecode Nothing _ _ = error "missing required argument: abi" +abidecode _ Nothing Nothing = error "must provide either calldata or returndata" +abidecode (Just abi) (Just calldata) Nothing = + let (sig, types) = parseAbi abi + abiSelector = strip0x (selector sig) + (dataSelector, dataBody) = + ByteString.splitAt 4 (hexByteString "--calldata" $ strip0x $ calldata) + values = decodeAbiValue (AbiTupleType (V.fromList types)) (Lazy.fromStrict $ dataBody) + in + if (abiSelector == dataSelector) then + show values + else + error $ "abi and calldata signatures do not match." + <> "\nabi: " <> show (ByteStringS abiSelector) + <> "\ncalldata: " <> show (ByteStringS dataSelector) + +abidecode (Just abi) Nothing (Just returndata) = + let (_, types) = parseAbiOutputs abi + dataBody = hexByteString "--returndata" $ strip0x $ returndata + values = decodeAbiValue (AbiTupleType (V.fromList types)) (Lazy.fromStrict $ dataBody) in show values +abidecode (Just abi) (Just calldata) (Just returndata) = + let inputs = abidecode (Just abi) (Just calldata) Nothing + outputs = abidecode (Just abi) Nothing (Just returndata) + in inputs <> " -> " <> outputs + abiselector :: Maybe String -> ByteString abiselector Nothing = error "missing required argument: abi" abiselector (Just abi) = selector $ fst $ parseAbi abi @@ -1013,6 +1035,18 @@ parseAbi abi = let sig = pack $ name <> (show (AbiTupleType (V.fromList types))) in (sig, types) +parseAbiOutputs :: String -> (Text, [AbiType]) +parseAbiOutputs abi = + case (abi ^? key "outputs" . _Array) of + Just outputs -> + (signature abi, snd <$> parseMethodInput <$> V.toList outputs) + Nothing -> + case parseSolidityAbi abi of + Left e -> error $ "could not parse abi fragment. result: " ++ e + Right (FunctionFragment name _ types _) -> + let sig = pack $ name <> (show (AbiTupleType (V.fromList types))) + in (sig, types) + abiencode :: Maybe String -> [String] -> ByteString abiencode Nothing _ = error "missing required argument: abi" abiencode (Just abijson) args = diff --git a/src/seth/libexec/seth/seth---abi-decode b/src/seth/libexec/seth/seth---abi-decode index aa8264aa9..cf8040895 100755 --- a/src/seth/libexec/seth/seth---abi-decode +++ b/src/seth/libexec/seth/seth---abi-decode @@ -1,37 +1,6 @@ -#!/usr/bin/env node -//seth---abi-decode -- extract return values from hexdata -const usage = `Usage: seth --abi-decode ()( -Decode according to ( are ignored).`; -if (process.argv.length == 4) { - if (process.argv[2].indexOf(')(') >= 0) { - // silence warnings we don't care about - const log = console.log - console.log = () => {}; - const ethers = require("./ethers.min.js"); - console.log = log; - const sig = process.argv[2].replace(')(', ') returns ('); - const hexdata = process.argv[3].indexOf('0x') == 0 ? process.argv[3] : "0x" + process.argv[3]; - try { - const funcs = new ethers.utils.Interface(['function ' + sig]).functions; - console.log(funcs[Object.keys(funcs)[0]].decode(hexdata).join('\n')) - } catch (e) { - console.error(e.toString()) - process.exit(1) - } - } else { - const {execFileSync} = require('child_process'); - try { - const yes = execFileSync("seth", - ["--to-hex", - process.argv[3]] - ).toString().replace('\n','') - console.log(yes) - } catch(e) { - console.error(e) - process.exit(1) - } - } -} else { - console.error(usage) - process.exit(1) -} +#!/usr/bin/env bash +### seth---abi-decode -- extract return values from hexdata +### Usage: seth --abi-decode ()( +### +### Decode according to ( are ignored). +hevm abidecode --abi "$1" --returndata "$2" diff --git a/src/seth/libexec/seth/seth---calldata-decode b/src/seth/libexec/seth/seth---calldata-decode index cc9d11e83..eedd0c58d 100755 --- a/src/seth/libexec/seth/seth---calldata-decode +++ b/src/seth/libexec/seth/seth---calldata-decode @@ -1,27 +1,6 @@ -#!/usr/bin/env node -const usage = `Usage: seth --calldata-decode ()( -Decode according to ( are ignored).`; -if (process.argv.length == 4 && - process.argv[2].indexOf('(') >= 0) { - // silence warnings we don't care about - const log = console.log - console.log = () => {}; - const ethers = require("./ethers.min.js"); - console.log = log; - const sig = process.argv[2]; - const calldata = process.argv[3]; - try { - const sighash = ethers.utils.hexDataSlice(calldata, 0, 4); - const data = ethers.utils.hexDataSlice(calldata, 4); - const funcs = new ethers.utils.Interface(["function " + sig.replace(')(', ') returns (')]).functions; - const func = Object.values(funcs).find(f => f.sighash == sighash); - const decoded = ethers.utils.defaultAbiCoder.decode(func.inputs, data); - console.log(decoded.map(e => e.toString()).join('\n')); - } catch (e) { - console.error(e.toString()) - process.exit(1) - } -} else { - console.error(usage) - process.exit(1) -} +#!/usr/bin/env bash +### seth---abi-decode -- extract return values from hexdata +### Usage: seth --abi-decode ()( +### +### Decode according to ( are ignored). +hevm abidecode --abi "$1" --calldata "$2" From 4b9a396be62029ef5eee601e8b6b98d8f97d2669 Mon Sep 17 00:00:00 2001 From: rain Date: Fri, 19 Nov 2021 08:05:12 +0000 Subject: [PATCH 4/8] hevm: generate abi json New command `hevm abi` to generate ABI json from a Solidity fragment. Works with functions, events and errors. Examples: seth abi "f(bool)" seth abi "function hello()" seth abi "event SomeEvent(bool x, string indexed s)" seth abi "error SomeError(string)" --- src/hevm/hevm-cli/hevm-cli.hs | 79 +++++++++----- src/hevm/hevm.cabal | 2 + src/hevm/src/EVM/ABI.hs | 68 +++++++++++- src/hevm/src/EVM/Solidity.hs | 13 ++- src/hevm/src/EVM/SolidityABI.hs | 103 ++++++++++++++---- .../libexec/seth/seth---abi-function-json | 9 +- src/seth/libexec/seth/seth-abi | 42 +++---- 7 files changed, 236 insertions(+), 80 deletions(-) diff --git a/src/hevm/hevm-cli/hevm-cli.hs b/src/hevm/hevm-cli/hevm-cli.hs index e39696cea..90c41f65e 100644 --- a/src/hevm/hevm-cli/hevm-cli.hs +++ b/src/hevm/hevm-cli/hevm-cli.hs @@ -59,7 +59,7 @@ import Data.ByteString (ByteString) import Data.List (intercalate, isSuffixOf) import Data.Tree import Data.Text (unpack, pack) -import Data.Text.Encoding (encodeUtf8) +import Data.Text.Encoding (encodeUtf8, decodeUtf8) import Data.Text.IO (hPutStr) import Data.Maybe (fromMaybe, fromJust) import Data.Version (showVersion) @@ -75,15 +75,17 @@ import qualified Data.Aeson as JSON import qualified Data.Aeson.Types as JSON import Data.Aeson (FromJSON (..), (.:)) import Data.Aeson.Lens hiding (values) -import qualified Data.Vector as V -import qualified Data.ByteString.Lazy as Lazy +import Data.Aeson.Encode.Pretty (encodePretty', Config(..), keyOrder, defConfig, Indent(..)) +import qualified Data.Vector as V +import qualified Data.ByteString.Lazy as Lazy import qualified Data.SBV as SBV import qualified Data.ByteString as ByteString import qualified Data.ByteString.Char8 as Char8 import qualified Data.ByteString.Lazy as LazyByteString import qualified Data.Map as Map import qualified Data.Text as Text +import qualified Data.Text.IO as TextIO import qualified System.Timeout as Timeout import qualified Paths_hevm as Paths @@ -229,6 +231,9 @@ data Command w { abi :: w ::: Maybe String "Signature of types to decode / encode" , arg :: w ::: [String] "Values to encode" } + | Abi + { abi :: w ::: Maybe String "Signature of types to decode / encode" + } | Abidecode { abi :: w ::: Maybe String "Signature of types to decode / encode" , calldata :: w ::: Maybe ByteString "Tx: calldata" @@ -332,6 +337,7 @@ main = do cmd <- Options.unwrapRecord "hevm -- Ethereum evaluator" let root = fromMaybe "." (dappRoot cmd) + required arg = fromMaybe (error ("missing required argument --" <> arg)) case cmd of Version {} -> putStrLn (showVersion Paths.version) Symbolic {} -> withCurrentDirectory root $ assert cmd @@ -339,13 +345,15 @@ main = do Exec {} -> launchExec cmd Abiencode {} -> - print . ByteStringS $ abiencode (abi cmd) (arg cmd) + print . ByteStringS $ abiencode (required "abi" (abi cmd)) (arg cmd) Abidecode {} -> - print $ abidecode (abi cmd) (calldata cmd) (returndata cmd) + TextIO.putStrLn $ abidecode (abi cmd) (calldata cmd) (returndata cmd) + Abi {} -> + TextIO.putStrLn $ makeabi $ required "abi" (abi cmd) Normalise {} -> - print $ normaliseSig (abi cmd) + TextIO.putStrLn $ normaliseSig $ required "abi" (abi cmd) Selector {} -> - print . ByteStringS $ abiselector (abi cmd) + print . ByteStringS $ abiselector $ required "abi" (abi cmd) BcTest {} -> launchTest cmd DappTest {} -> @@ -987,13 +995,12 @@ runVMTest diffmode mode timelimit (name, x) = #endif -normaliseSig :: Maybe String -> Text -normaliseSig Nothing = error "missing required argument: abi" -normaliseSig (Just abi) = fst $ parseAbi abi +normaliseSig :: String -> Text +normaliseSig abi = fst $ parseAbi abi -abidecode :: Maybe String -> Maybe ByteString -> Maybe ByteString -> String -abidecode Nothing _ _ = error "missing required argument: abi" -abidecode _ Nothing Nothing = error "must provide either calldata or returndata" +abidecode :: Maybe String -> Maybe ByteString -> Maybe ByteString -> Text +abidecode Nothing _ _ = error "missing required argument --abi" +abidecode _ Nothing Nothing = error "must provide either --calldata or --returndata" abidecode (Just abi) (Just calldata) Nothing = let (sig, types) = parseAbi abi abiSelector = strip0x (selector sig) @@ -1002,7 +1009,7 @@ abidecode (Just abi) (Just calldata) Nothing = values = decodeAbiValue (AbiTupleType (V.fromList types)) (Lazy.fromStrict $ dataBody) in if (abiSelector == dataSelector) then - show values + pack $ show values else error $ "abi and calldata signatures do not match." <> "\nabi: " <> show (ByteStringS abiSelector) @@ -1012,16 +1019,36 @@ abidecode (Just abi) Nothing (Just returndata) = let (_, types) = parseAbiOutputs abi dataBody = hexByteString "--returndata" $ strip0x $ returndata values = decodeAbiValue (AbiTupleType (V.fromList types)) (Lazy.fromStrict $ dataBody) - in show values + in pack $ show values abidecode (Just abi) (Just calldata) (Just returndata) = let inputs = abidecode (Just abi) (Just calldata) Nothing outputs = abidecode (Just abi) Nothing (Just returndata) in inputs <> " -> " <> outputs -abiselector :: Maybe String -> ByteString -abiselector Nothing = error "missing required argument: abi" -abiselector (Just abi) = selector $ fst $ parseAbi abi +abiselector :: String -> ByteString +abiselector abi = selector $ fst $ parseAbi abi + +makeabi :: String -> Text +makeabi abi = + case parseSolidityAbi abi of + Left e -> error $ "could not parse abi fragment. result: " ++ e + Right (Just method, _, _) -> render (JSON.toJSON method) + Right (Nothing, Just event, _) -> render (JSON.toJSON event) + Right (Nothing, Nothing, Just err) -> render (JSON.toJSON err) + where + render = decodeUtf8 . Lazy.toStrict . encoder + encoder = encodePretty' $ + defConfig { confIndent = Spaces 2, + confCompare = keyOrder [ "type" + , "name" + , "indexed" + , "stateMutability" + , "anonymous" + , "components" + , "inputs" + , "outputs" + ]} parseAbi :: String -> (Text, [AbiType]) parseAbi abi = @@ -1031,9 +1058,10 @@ parseAbi abi = Nothing -> case parseSolidityAbi abi of Left e -> error $ "could not parse abi fragment. result: " ++ e - Right (FunctionFragment name types _ _) -> - let sig = pack $ name <> (show (AbiTupleType (V.fromList types))) - in (sig, types) + Right (Just (Method _ types _ sig _), _, _) -> (sig, snd <$> types) + Right (Nothing, Nothing, Just (SolError name types)) -> (sig, types) + where sig = name <> pack (show (AbiTupleType (V.fromList types))) + Right (_, Just _, _) -> error "event encoding is not currently supported" parseAbiOutputs :: String -> (Text, [AbiType]) parseAbiOutputs abi = @@ -1043,13 +1071,10 @@ parseAbiOutputs abi = Nothing -> case parseSolidityAbi abi of Left e -> error $ "could not parse abi fragment. result: " ++ e - Right (FunctionFragment name _ types _) -> - let sig = pack $ name <> (show (AbiTupleType (V.fromList types))) - in (sig, types) + Right (Just (Method outputs _ _ sig _), _, _) -> (sig, snd <$> outputs) -abiencode :: Maybe String -> [String] -> ByteString -abiencode Nothing _ = error "missing required argument: abi" -abiencode (Just abijson) args = +abiencode :: String -> [String] -> ByteString +abiencode abijson args = let (sig', declarations) = parseAbi abijson in if length declarations == length args then abiMethod sig' $ AbiTuple . V.fromList $ zipWith makeAbiValue declarations args diff --git a/src/hevm/hevm.cabal b/src/hevm/hevm.cabal index 50d3d017f..9aeabd5eb 100644 --- a/src/hevm/hevm.cabal +++ b/src/hevm/hevm.cabal @@ -90,6 +90,7 @@ library tree-view >= 0.5 && < 0.6, abstract-par >= 0.3.3 && < 0.4, aeson >= 1.5.6 && < 1.6, + aeson-pretty, bytestring >= 0.10.8 && < 0.11, scientific >= 0.3.6 && < 0.4, binary >= 0.8.6 && < 0.9, @@ -164,6 +165,7 @@ executable hevm build-depends: QuickCheck, aeson, + aeson-pretty, ansi-wl-pprint, async, base, diff --git a/src/hevm/src/EVM/ABI.hs b/src/hevm/src/EVM/ABI.hs index a06f3a69b..9fc5e22c9 100644 --- a/src/hevm/src/EVM/ABI.hs +++ b/src/hevm/src/EVM/ABI.hs @@ -60,6 +60,7 @@ module EVM.ABI import EVM.Types import Control.Monad (replicateM, replicateM_, forM_, void) +import Data.Aeson hiding (json) import Data.Binary.Get (Get, runGet, runGetOrFail, label, getWord8, getWord32be, skip) import Data.Binary.Put (Put, runPut, putWord8, putWord32be) import Data.Bits (shiftL, shiftR, (.&.)) @@ -135,11 +136,30 @@ data AbiType | AbiArrayDynamicType AbiType | AbiArrayType Int AbiType | AbiTupleType (Vector AbiType) + | AbiNamedTupleType (Vector (Text, AbiType)) deriving (Read, Eq, Ord, Generic) instance Show AbiType where show = Text.unpack . abiTypeSolidity +instance ToJSON AbiType where + toJSON abitype = case abitype of + AbiNamedTupleType tuple -> + toJSON [ obj name typ | (name, typ) <- Vector.toList tuple ] + AbiTupleType tuple -> + toJSON [ obj "" typ | typ <- Vector.toList tuple ] + _ -> + toJSON $ Text.pack $ show abitype + where + obj name (AbiNamedTupleType ts) = + object [ "type" .= String "tuple", "name" .= String name, + "components" .= toJSON (AbiNamedTupleType ts) ] + obj name (AbiTupleType ts) = + object [ "type" .= String "tuple", "name" .= String name, + "components" .= toJSON (AbiTupleType ts) ] + obj name typ = + object [ "type" .= (abiTypeSolidity typ), "name" .= String name ] + data AbiKind = Dynamic | Static deriving (Show, Read, Eq, Ord, Generic) @@ -149,9 +169,45 @@ data Indexed = Indexed | NotIndexed deriving (Show, Ord, Eq, Generic) data Event = Event Text Anonymity [(Text, AbiType, Indexed)] deriving (Show, Ord, Eq, Generic) -data SolError = SolError Text [AbiType] +data SolError = SolError Text [AbiType] -- todo: should be (Text, AbiType) with argnames deriving (Show, Ord, Eq, Generic) +instance ToJSON Event where + toJSON (Event name anon indexedtypes) = + object [ "type" .= String "event" + , "name" .= name + , "anonymous" .= isAnon anon + , "inputs" .= inputs + ] + where + isIndexed = \case + Indexed -> True + NotIndexed -> False + isAnon = \case + Anonymous -> True + NotAnonymous -> False + + inputs = toJSON [ obj argname typ index | (argname, typ, index) <- indexedtypes ] + + obj argname (AbiNamedTupleType ts) index = + object [ "type" .= String "tuple", "name" .= argname, + "indexed" .= isIndexed index, + "components" .= toJSON (AbiNamedTupleType ts) ] + obj argname (AbiTupleType ts) index = + object [ "type" .= String "tuple", "name" .= argname, + "indexed" .= isIndexed index, + "components" .= toJSON (AbiTupleType ts) ] + obj argname typ index = + object [ "type" .= abiTypeSolidity typ, "name" .= argname, + "indexed" .= isIndexed index ] + +instance ToJSON SolError where + toJSON (SolError name types) = + object [ "type" .= String "error" + , "name" .= name + , "inputs" .= (toJSON $ AbiTupleType (Vector.fromList types)) + ] + abiKind :: AbiType -> AbiKind abiKind = \case AbiBytesDynamicType -> Dynamic @@ -159,6 +215,7 @@ abiKind = \case AbiArrayDynamicType _ -> Dynamic AbiArrayType _ t -> abiKind t AbiTupleType ts -> if Dynamic `elem` (abiKind <$> ts) then Dynamic else Static + AbiNamedTupleType ts -> if Dynamic `elem` (abiKind <$> snd <$> ts) then Dynamic else Static _ -> Static abiValueType :: AbiValue -> AbiType @@ -186,6 +243,7 @@ abiTypeSolidity = \case AbiArrayDynamicType t -> abiTypeSolidity t <> "[]" AbiArrayType n t -> abiTypeSolidity t <> "[" <> pack (show n) <> "]" AbiTupleType ts -> "(" <> (Text.intercalate "," . Vector.toList $ abiTypeSolidity <$> ts) <> ")" + AbiNamedTupleType ts -> "(" <> (Text.intercalate "," . Vector.toList $ abiTypeSolidity <$> snd <$> ts) <> ")" getAbi :: AbiType -> Get AbiValue getAbi t = label (Text.unpack (abiTypeSolidity t)) $ @@ -223,6 +281,9 @@ getAbi t = label (Text.unpack (abiTypeSolidity t)) $ AbiTupleType ts -> AbiTuple <$> getAbiSeq (Vector.length ts) (Vector.toList ts) + AbiNamedTupleType ts -> + AbiTuple <$> getAbiSeq (Vector.length ts) (Vector.toList $ snd <$> ts) + putAbi :: AbiValue -> Put putAbi = \case AbiUInt _ x -> @@ -428,6 +489,8 @@ genAbiValue = \case replicateM n (scale (`div` 2) (genAbiValue t)) AbiTupleType ts -> AbiTuple <$> mapM genAbiValue ts + AbiNamedTupleType ts -> + AbiTuple <$> mapM genAbiValue (snd <$> ts) where genUInt n = AbiUInt n <$> arbitraryIntegralWithMax (2^n-1) @@ -515,6 +578,9 @@ parseAbiValue (AbiArrayType n typ) = parseAbiValue (AbiTupleType types) = AbiTuple <$> do args <- tupleP [parseAbiValue typ | typ <- Vector.toList types] return $ Vector.fromList args +parseAbiValue (AbiNamedTupleType types) = + AbiTuple <$> do args <- tupleP [parseAbiValue typ | typ <- Vector.toList $ snd <$> types] + return $ Vector.fromList args tupleP :: [ReadP a] -> ReadP [a] tupleP parsers = between (char '(') (char ')') (argP parsers) diff --git a/src/hevm/src/EVM/Solidity.hs b/src/hevm/src/EVM/Solidity.hs index b7d0f36bf..36ea57927 100644 --- a/src/hevm/src/EVM/Solidity.hs +++ b/src/hevm/src/EVM/Solidity.hs @@ -83,7 +83,7 @@ import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as NonEmpty import Data.Semigroup import Data.Sequence (Seq) -import Data.Text (Text, pack, intercalate) +import Data.Text (Text, pack, intercalate, toLower) import Data.Text.Encoding (encodeUtf8, decodeUtf8) import Data.Text.IO (readFile, writeFile) import Data.Vector (Vector) @@ -161,6 +161,17 @@ data Method = Method , _methodMutability :: Mutability } deriving (Show, Eq, Ord, Generic) +instance ToJSON Method where + toJSON (Method outputs inputs name _ mutability) = + object [ "type" .= String "function" + , "name" .= name + , "stateMutability" .= (toLower $ pack $ show $ mutability) + , "inputs" .= + (toJSON $ AbiNamedTupleType (Vector.fromList inputs)) + , "outputs" .= + (toJSON $ AbiNamedTupleType (Vector.fromList outputs)) + ] + data Mutability = Pure -- ^ specified to not read blockchain state | View -- ^ specified to not modify the blockchain state diff --git a/src/hevm/src/EVM/SolidityABI.hs b/src/hevm/src/EVM/SolidityABI.hs index 6c8f8e549..66591b363 100644 --- a/src/hevm/src/EVM/SolidityABI.hs +++ b/src/hevm/src/EVM/SolidityABI.hs @@ -1,25 +1,25 @@ -module EVM.SolidityABI ( parseSolidityAbi , Fragment (..) ) where +module EVM.SolidityABI ( parseSolidityAbi ) where -import EVM.ABI +import EVM.ABI (AbiType(..), Event(..), Anonymity(..), Indexed(..), SolError(..)) +import EVM.Solidity (Method(..), Mutability(..)) import Control.Monad (when) import Data.Char (isDigit, isAlphaNum, isAlpha) +import Data.Text (Text, pack) import Text.ParserCombinators.ReadP import qualified Data.Vector as V -data Fragment = FunctionFragment - { _name :: String - , _types :: [AbiType] - , _returns :: [AbiType] - , _modifiers :: [String] - } - deriving Show - -parseSolidityAbi :: String -> Either String Fragment +parseSolidityAbi :: String -> Either String (Maybe Method, Maybe Event, Maybe SolError) parseSolidityAbi abi = - case readP_to_S parseFragment abi of - [(val,"")] -> Right val + case readP_to_S parseFunction abi of + [(val,"")] -> Right (Just val, Nothing, Nothing) + [] -> case readP_to_S parseEvent abi of + [(val,"")] -> Right (Nothing, Just val, Nothing) + [] -> case readP_to_S parseError abi of + [(val,"")] -> Right (Nothing, Nothing, Just val) + r -> Left (show r) + r -> Left (show r) r -> Left (show r) isName1 :: Char -> Bool @@ -43,8 +43,8 @@ excluding p x = do when (elem name x) pfail return name -parseFragment :: ReadP Fragment -parseFragment = do +parseFunction :: ReadP Method +parseFunction = do optional (skipSpaces *> string "function" *> space) name <- (skipSpaces *> identifier) types <- (skipSpaces *> parseTypes) @@ -58,24 +58,79 @@ parseFragment = do optional (char ';') skipSpaces eof - return (FunctionFragment name types returns modifiers) + let mutability = + case (flip elem modifiers) <$> ["view", "pure", "payable"] of + [False, False, False] -> NonPayable + [True, False, False] -> View + [False, True, False] -> Pure + [False, False, True ] -> Payable + _ -> error "overspecified mutability" + return $ Method + (fst <$> returns) + (fst <$> types) + (pack name) + (pack $ name <> (show (AbiTupleType (V.fromList (snd <$> fst <$> types))))) + mutability + +parseError :: ReadP SolError +parseError = do + (skipSpaces *> string "error" *> space) + name <- (skipSpaces *> identifier) + types <- (skipSpaces *> parseTypes) + skipSpaces + optional (char ';') + skipSpaces + eof + return $ SolError (pack name) (snd <$> fst <$> types) + +data Location + = MemoryLocation + | CalldataLocation + | IndexedLocation + | NoLocation -parseTypes :: ReadP [AbiType] +parseEvent :: ReadP Event +parseEvent = do + skipSpaces *> string "event" *> space + name <- (skipSpaces *> identifier) + types <- (skipSpaces *> parseTypes) + anon <- option "" (space *> string "anonymous") + skipSpaces + optional (char ';') + skipSpaces + eof + let + anonymous = \case + "anonymous" -> Anonymous + "" -> NotAnonymous + indexed = \case + ((argname, typ), IndexedLocation) -> (argname, typ, Indexed) + ((argname, typ), _) -> (argname, typ, NotIndexed) + return $ EVM.ABI.Event (pack name) (anonymous anon) (indexed <$> types) + +parseTypes :: ReadP [((Text, AbiType), Location)] parseTypes = between (char '(' *> skipSpaces) (skipSpaces <* char ')') (sepBy parseType (skipSpaces <* char ',' *> skipSpaces)) -parseType :: ReadP AbiType +parseType :: ReadP ((Text, AbiType), Location) parseType = do t <- parseBasicType s <- many (between (char '[') (char ']') (munch isDigit)) - optional (space *> location) - optional (space *> argName) - return (foldl makeArray t s) + loc <- option "" (space *> location) + name <- option "" (space *> argName) + return ((pack name, foldl makeArray t s), locate loc) where - location = (string "memory" +++ string "calldata") - argName = identifier `excluding` ["memory", "calldata"] + location = (string "memory" +++ string "calldata" +++ string "indexed") + locate = \case + "memory" -> MemoryLocation + "calldata" -> CalldataLocation + "indexed" -> IndexedLocation + "" -> NoLocation + _ -> error "unknown location" + + argName = identifier `excluding` ["memory", "calldata", "indexed"] makeArray :: AbiType -> String -> AbiType makeArray t "" = AbiArrayDynamicType t @@ -94,5 +149,5 @@ parseType = do , (<++) (AbiIntType <$> (string "int" *> (read <$> munch1 isDigit))) (AbiIntType 256 <$ string "int") - , AbiTupleType <$> V.fromList <$> (optional identifier *> parseTypes) + , AbiNamedTupleType <$> fst <$> V.unzip <$> V.fromList <$> (optional identifier *> parseTypes) ] diff --git a/src/seth/libexec/seth/seth---abi-function-json b/src/seth/libexec/seth/seth---abi-function-json index 9e03eae63..f7610d383 100755 --- a/src/seth/libexec/seth/seth---abi-function-json +++ b/src/seth/libexec/seth/seth---abi-function-json @@ -1,9 +1,4 @@ #!/usr/bin/env bash +# todo: remove? redundant now that seth abi handles this all set -e -if [[ $1 = *\)\(* ]]; then - sig=${1/)(/) public returns (} -else - sig="$1 public" -fi - -seth abi "function $sig" | jq '.[0]' -c +seth abi "$1" | jq -c diff --git a/src/seth/libexec/seth/seth-abi b/src/seth/libexec/seth/seth-abi index 699dacd56..0703e4cb9 100755 --- a/src/seth/libexec/seth/seth-abi +++ b/src/seth/libexec/seth/seth-abi @@ -1,22 +1,24 @@ -#!/usr/bin/env node +#!/usr/bin/env bash +### seth-abi -- Generate ABI json for a given signature +### Usage: seth abi +### Example: seth abi "f(uint x)" +### Returns: { +### "type": "function", +### "name": "f", +### "stateMutability": "nonpayable", +### "inputs": [ +### { +### "type": "uint256", +### "name": "x" +### } +### ], +### "outputs": [] +### } +### Example: seth abi "function f(bool, MyStruct(uint, bytes[][2])) returns (string)" +### Example: seth abi "event Event(uint32 x, bool indexed b) +### Example: seth abi "error SomeError(string msg)" -// silence warnings we don't care about -const log = console.log -console.log = () => {}; +set -e +[[ $# = 1 ]] || seth --fail-usage "$0" -const ethers = require("./ethers.min.js"); - -console.log = log; - -const sig = process.argv[2]; -if (!sig) { - console.error("Usage: seth-abi ") - process.exit(1); -} - -try { - console.log(JSON.stringify([ethers.utils.parseSignature(sig)])); -} catch (e) { - console.error(`seth-abi: error: ${e.message}`); - process.exit(1); -} +hevm abi --abi "$1" From 784e82ae70ec39a3f19661a147f773fb83dbaa4f Mon Sep 17 00:00:00 2001 From: rain Date: Mon, 22 Nov 2021 05:05:02 +0000 Subject: [PATCH 5/8] doc --- src/seth/libexec/seth/seth---calldata-decode | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/seth/libexec/seth/seth---calldata-decode b/src/seth/libexec/seth/seth---calldata-decode index eedd0c58d..70371067d 100755 --- a/src/seth/libexec/seth/seth---calldata-decode +++ b/src/seth/libexec/seth/seth---calldata-decode @@ -1,6 +1,6 @@ #!/usr/bin/env bash -### seth---abi-decode -- extract return values from hexdata -### Usage: seth --abi-decode ()( +### seth---calldata-decode -- extract calldata values from hexdata +### Usage: seth --calldata-decode ()( ### -### Decode according to ( are ignored). +### Decode according to ( are ignored). hevm abidecode --abi "$1" --calldata "$2" From e472e4b1b6b32cb887f977a5b8b9683ae6a4bea1 Mon Sep 17 00:00:00 2001 From: rain Date: Tue, 23 Nov 2021 03:42:27 +0000 Subject: [PATCH 6/8] hevm: split abidecode results over multiple lines --- src/hevm/hevm-cli/hevm-cli.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hevm/hevm-cli/hevm-cli.hs b/src/hevm/hevm-cli/hevm-cli.hs index 90c41f65e..8fc86acb7 100644 --- a/src/hevm/hevm-cli/hevm-cli.hs +++ b/src/hevm/hevm-cli/hevm-cli.hs @@ -1006,10 +1006,10 @@ abidecode (Just abi) (Just calldata) Nothing = abiSelector = strip0x (selector sig) (dataSelector, dataBody) = ByteString.splitAt 4 (hexByteString "--calldata" $ strip0x $ calldata) - values = decodeAbiValue (AbiTupleType (V.fromList types)) (Lazy.fromStrict $ dataBody) + AbiTuple (values) = decodeAbiValue (AbiTupleType (V.fromList types)) (Lazy.fromStrict $ dataBody) in if (abiSelector == dataSelector) then - pack $ show values + pack $ intercalate "\n" (show <$> V.toList values) else error $ "abi and calldata signatures do not match." <> "\nabi: " <> show (ByteStringS abiSelector) @@ -1018,13 +1018,13 @@ abidecode (Just abi) (Just calldata) Nothing = abidecode (Just abi) Nothing (Just returndata) = let (_, types) = parseAbiOutputs abi dataBody = hexByteString "--returndata" $ strip0x $ returndata - values = decodeAbiValue (AbiTupleType (V.fromList types)) (Lazy.fromStrict $ dataBody) - in pack $ show values + AbiTuple values = decodeAbiValue (AbiTupleType (V.fromList types)) (Lazy.fromStrict $ dataBody) + in pack $ intercalate "\n" (show <$> V.toList values) abidecode (Just abi) (Just calldata) (Just returndata) = let inputs = abidecode (Just abi) (Just calldata) Nothing outputs = abidecode (Just abi) Nothing (Just returndata) - in inputs <> " -> " <> outputs + in inputs <> "\n->\n" <> outputs abiselector :: String -> ByteString abiselector abi = selector $ fst $ parseAbi abi From 50953d868a3ae99e6904fabbda2963e84d9f84b5 Mon Sep 17 00:00:00 2001 From: rain Date: Tue, 23 Nov 2021 04:04:35 +0000 Subject: [PATCH 7/8] seth-lookup-address: dequote call result --- src/seth/libexec/seth/seth-lookup-address | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/seth/libexec/seth/seth-lookup-address b/src/seth/libexec/seth/seth-lookup-address index 219a29580..a33ba6ad1 100755 --- a/src/seth/libexec/seth/seth-lookup-address +++ b/src/seth/libexec/seth/seth-lookup-address @@ -26,9 +26,10 @@ namehash=0x$(seth namehash $namein | cut -c3-) ENS_REGISTRY=${SETH_ENS_REGISTRY:-'0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'} # same on all supported networks resolver=$(seth call $ENS_REGISTRY "resolver(bytes32)(address)" $namehash) name=$(seth call $resolver "name(bytes32)(string)" $namehash) +name=${name//\"/} # dequote # check the reverse direction and make sure the addresses match -address=$(seth resolve-name $name) +address=$(seth resolve-name "$name") if [[ $(seth --to-hexdata $address) != $addressin ]]; then seth --fail "${0##*/}: error: forward resolution of the found ENS name $name did not match" fi From 933fb30c404fe8126e70deeba867413a8377e793 Mon Sep 17 00:00:00 2001 From: MrChico Date: Tue, 7 Dec 2021 20:05:42 +0100 Subject: [PATCH 8/8] Apply suggestions from code review --- src/hevm/shell.nix | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/hevm/shell.nix b/src/hevm/shell.nix index bc80d5bd0..ad6098215 100644 --- a/src/hevm/shell.nix +++ b/src/hevm/shell.nix @@ -7,12 +7,8 @@ let p.hevm ]; buildInputs = with pkgs.haskellPackages; with pkgs; [ - ghci - ghcid cabal-install haskell-language-server - bc coreutils curl ethsign git gnused nix jq hevm jshon nodejs tre perl solc - gnugrep ]; withHoogle = true; };