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/hevm-cli/hevm-cli.hs b/src/hevm/hevm-cli/hevm-cli.hs index 6fdd91e72..8fc86acb7 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) @@ -58,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) @@ -74,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 @@ -228,6 +231,20 @@ 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" + , returndata :: w ::: Maybe ByteString "returndata" + } + | 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" } @@ -320,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 @@ -327,7 +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 {} -> + TextIO.putStrLn $ abidecode (abi cmd) (calldata cmd) (returndata cmd) + Abi {} -> + TextIO.putStrLn $ makeabi $ required "abi" (abi cmd) + Normalise {} -> + TextIO.putStrLn $ normaliseSig $ required "abi" (abi cmd) + Selector {} -> + print . ByteStringS $ abiselector $ required "abi" (abi cmd) BcTest {} -> launchTest cmd DappTest {} -> @@ -968,16 +994,87 @@ 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 -abiencode Nothing _ = error "missing required argument: abi" -abiencode (Just abijson) args = +normaliseSig :: String -> Text +normaliseSig abi = fst $ parseAbi abi + +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) + (dataSelector, dataBody) = + ByteString.splitAt 4 (hexByteString "--calldata" $ strip0x $ calldata) + AbiTuple (values) = decodeAbiValue (AbiTupleType (V.fromList types)) (Lazy.fromStrict $ dataBody) + in + if (abiSelector == dataSelector) then + pack $ intercalate "\n" (show <$> V.toList 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 + 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 <> "\n->\n" <> outputs + +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 = + 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 (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 = + 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 (Just (Method outputs _ _ sig _), _, _) -> (sig, snd <$> outputs) + +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 cf36961e6..9aeabd5eb 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, @@ -89,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, @@ -163,6 +165,7 @@ executable hevm build-depends: QuickCheck, aeson, + aeson-pretty, ansi-wl-pprint, async, base, diff --git a/src/hevm/shell.nix b/src/hevm/shell.nix index d3ed04b0c..ad6098215 100644 --- a/src/hevm/shell.nix +++ b/src/hevm/shell.nix @@ -2,12 +2,11 @@ let inherit (dapphub) pkgs; - drv = pkgs.haskellPackages.shellFor { packages = p: [ p.hevm ]; - buildInputs = with pkgs.haskellPackages; [ + buildInputs = with pkgs.haskellPackages; with pkgs; [ cabal-install haskell-language-server ]; diff --git a/src/hevm/src/EVM/ABI.hs b/src/hevm/src/EVM/ABI.hs index b459b2073..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) @@ -512,7 +575,30 @@ 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 +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) + +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 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 new file mode 100644 index 000000000..66591b363 --- /dev/null +++ b/src/hevm/src/EVM/SolidityABI.hs @@ -0,0 +1,153 @@ +module EVM.SolidityABI ( parseSolidityAbi ) where + +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 + +parseSolidityAbi :: String -> Either String (Maybe Method, Maybe Event, Maybe SolError) +parseSolidityAbi abi = + 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 +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 + +parseFunction :: ReadP Method +parseFunction = 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 + 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 + +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 ((Text, AbiType), Location) +parseType = do + t <- parseBasicType + s <- many (between (char '[') (char ']') (munch isDigit)) + loc <- option "" (space *> location) + name <- option "" (space *> argName) + return ((pack name, foldl makeArray t s), locate loc) + where + 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 + 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") + , AbiNamedTupleType <$> fst <$> V.unzip <$> V.fromList <$> (optional identifier *> parseTypes) + ] 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---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-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---calldata-decode b/src/seth/libexec/seth/seth---calldata-decode index cc9d11e83..70371067d 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---calldata-decode -- extract calldata values from hexdata +### Usage: seth --calldata-decode ()( +### +### Decode according to ( are ignored). +hevm abidecode --abi "$1" --calldata "$2" 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" 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-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 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}"