diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f46284a9..0e91daefb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ## Added +- Support for Foundry/Hardhat `console.log` — calls to the console address are intercepted and decoded in traces - Source location info (file, line, code snippet) is now shown in state merge debug messages - New opcode: CLZ - New POr/PAnd/PImpl/Or/And simplification rules diff --git a/hevm.cabal b/hevm.cabal index ee0cf9835..ffad0e609 100644 --- a/hevm.cabal +++ b/hevm.cabal @@ -101,6 +101,7 @@ library EVM.Solvers, EVM.Exec, EVM.Format, + EVM.ConsoleLog, EVM.Fetch, EVM.FeeSchedule, EVM.Op, diff --git a/src/EVM.hs b/src/EVM.hs index cc23831e9..ef672b92c 100644 --- a/src/EVM.hs +++ b/src/EVM.hs @@ -578,7 +578,7 @@ exec1 conf = do Just b -> pushSym (bufLength b) Nothing -> pushSym $ CodeSize x case x of - a@(LitAddr _) -> if a == cheatCode + a@(LitAddr _) -> if a == cheatCode || a == consoleAddr then do next assign' (#state % #stack) xs @@ -1858,6 +1858,11 @@ accessStorageForGas addr key = do cheatCode :: Expr EAddr cheatCode = LitAddr $ unsafeInto (keccak' "hevm cheat code") +-- | The address used by Foundry/Hardhat console.log +-- 0x000000000000000000636F6e736F6c652e6c6f67 (ASCII "console.log") +consoleAddr :: Expr EAddr +consoleAddr = LitAddr 0x000000000000000000636F6e736F6c652e6c6f67 + cheat :: forall t . (?conf :: Config, ?op :: Word8, VMOps t, Typeable t) => Gas t -> (Expr EWord, Expr EWord) -> (Expr EWord, Expr EWord) -> [Expr EWord] @@ -2307,6 +2312,12 @@ delegateCall this gasGiven xTo xContext xValue xInOffset xInSize xOutOffset xOut precompiledContract this gasGiven xTo' xContext' xValue xInOffset xInSize xOutOffset xOutSize xs | xTo == cheatCode = do cheat gasGiven (xInOffset, xInSize) (xOutOffset, xOutSize) xs + | xTo == consoleAddr = do + calldata <- readMemory xInOffset xInSize + pushTrace $ ConsoleLog calldata + assign' (#state % #stack) (Lit 1 : xs) + assign (#state % #returndata) mempty + next | otherwise = callChecks this gasGiven xContext xTo xValue xInOffset xInSize xOutOffset xOutSize xs $ \xGas -> do diff --git a/src/EVM/ConsoleLog.hs b/src/EVM/ConsoleLog.hs new file mode 100644 index 000000000..97fad7926 --- /dev/null +++ b/src/EVM/ConsoleLog.hs @@ -0,0 +1,189 @@ +module EVM.ConsoleLog + ( formatConsoleLog + ) where + +import Data.ByteString (ByteString) +import Data.ByteString qualified as BS +import Data.ByteString.Char8 qualified as Char8 +import Data.ByteString.Builder (byteStringHex, toLazyByteString) +import Data.ByteString.Lazy (toStrict) +import Data.Map.Strict (Map) +import Data.Map.Strict qualified as Map +import Data.Text (Text, pack, intercalate) +import Data.Text qualified as T +import Data.Text.Encoding qualified as T + +import EVM.ABI (AbiType(..), AbiValue(..), decodeBuf, AbiVals(..)) +import EVM.Types (Expr(..), EType(..), FunctionSelector(..), abiKeccak, word32) + +-- | Try to decode and format a console.log calldata buffer. +-- Returns a human-readable representation of the log message. +formatConsoleLog :: Expr Buf -> Text +formatConsoleLog (ConcreteBuf bs) = formatConsoleLogBS bs +formatConsoleLog _ = "" + +formatConsoleLogBS :: ByteString -> Text +formatConsoleLogBS bs + | BS.length bs < 4 = "console::log()" + | otherwise = + let selector = FunctionSelector (word32 (BS.unpack (BS.take 4 bs))) + payload = BS.drop 4 bs + in case Map.lookup selector consoleLogSigs of + Just (_, types) -> + case decodeBuf types (ConcreteBuf payload) of + (CAbi vals, _) -> "console::log(" <> intercalate ", " (map showAbiVal vals) <> ")" + _ -> "console::log(" <> hexBS bs <> ")" + Nothing -> "console::log(" <> hexBS bs <> ")" + where + hexBS b = "0x" <> T.decodeUtf8 (toStrict (toLazyByteString (byteStringHex b))) + +-- | Format an ABI value for console output +showAbiVal :: AbiValue -> Text +showAbiVal (AbiString s) = T.pack (show (Char8.unpack s)) +showAbiVal (AbiAddress addr) = pack (show addr) +showAbiVal (AbiBool b) = if b then "true" else "false" +showAbiVal v = pack (show v) + +-- | Map from 4-byte selector to (display name, parameter types) for all +-- console.log overloads. +consoleLogSigs :: Map FunctionSelector (Text, [AbiType]) +consoleLogSigs = Map.fromList + [ sig "log()" [] + -- single param + , sig "log(bool)" [AbiBoolType] + , sig "log(address)" [AbiAddressType] + , sig "log(uint256)" [AbiUIntType 256] + , sig "log(int256)" [AbiIntType 256] + , sig "log(string)" [AbiStringType] + , sig "log(bytes)" [AbiBytesDynamicType] + -- two params + , sig "log(bool,bool)" [AbiBoolType, AbiBoolType] + , sig "log(bool,address)" [AbiBoolType, AbiAddressType] + , sig "log(bool,uint256)" [AbiBoolType, AbiUIntType 256] + , sig "log(bool,string)" [AbiBoolType, AbiStringType] + , sig "log(address,bool)" [AbiAddressType, AbiBoolType] + , sig "log(address,address)" [AbiAddressType, AbiAddressType] + , sig "log(address,uint256)" [AbiAddressType, AbiUIntType 256] + , sig "log(address,string)" [AbiAddressType, AbiStringType] + , sig "log(uint256,bool)" [AbiUIntType 256, AbiBoolType] + , sig "log(uint256,address)" [AbiUIntType 256, AbiAddressType] + , sig "log(uint256,uint256)" [AbiUIntType 256, AbiUIntType 256] + , sig "log(uint256,string)" [AbiUIntType 256, AbiStringType] + , sig "log(string,bool)" [AbiStringType, AbiBoolType] + , sig "log(string,address)" [AbiStringType, AbiAddressType] + , sig "log(string,uint256)" [AbiStringType, AbiUIntType 256] + , sig "log(string,string)" [AbiStringType, AbiStringType] + , sig "log(string,int256)" [AbiStringType, AbiIntType 256] + -- three params + , sig "log(bool,bool,bool)" [AbiBoolType, AbiBoolType, AbiBoolType] + , sig "log(bool,bool,address)" [AbiBoolType, AbiBoolType, AbiAddressType] + , sig "log(bool,bool,uint256)" [AbiBoolType, AbiBoolType, AbiUIntType 256] + , sig "log(bool,bool,string)" [AbiBoolType, AbiBoolType, AbiStringType] + , sig "log(bool,address,bool)" [AbiBoolType, AbiAddressType, AbiBoolType] + , sig "log(bool,address,address)" [AbiBoolType, AbiAddressType, AbiAddressType] + , sig "log(bool,address,uint256)" [AbiBoolType, AbiAddressType, AbiUIntType 256] + , sig "log(bool,address,string)" [AbiBoolType, AbiAddressType, AbiStringType] + , sig "log(bool,uint256,bool)" [AbiBoolType, AbiUIntType 256, AbiBoolType] + , sig "log(bool,uint256,address)" [AbiBoolType, AbiUIntType 256, AbiAddressType] + , sig "log(bool,uint256,uint256)" [AbiBoolType, AbiUIntType 256, AbiUIntType 256] + , sig "log(bool,uint256,string)" [AbiBoolType, AbiUIntType 256, AbiStringType] + , sig "log(bool,string,bool)" [AbiBoolType, AbiStringType, AbiBoolType] + , sig "log(bool,string,address)" [AbiBoolType, AbiStringType, AbiAddressType] + , sig "log(bool,string,uint256)" [AbiBoolType, AbiStringType, AbiUIntType 256] + , sig "log(bool,string,string)" [AbiBoolType, AbiStringType, AbiStringType] + , sig "log(address,bool,bool)" [AbiAddressType, AbiBoolType, AbiBoolType] + , sig "log(address,bool,address)" [AbiAddressType, AbiBoolType, AbiAddressType] + , sig "log(address,bool,uint256)" [AbiAddressType, AbiBoolType, AbiUIntType 256] + , sig "log(address,bool,string)" [AbiAddressType, AbiBoolType, AbiStringType] + , sig "log(address,address,bool)" [AbiAddressType, AbiAddressType, AbiBoolType] + , sig "log(address,address,address)" [AbiAddressType, AbiAddressType, AbiAddressType] + , sig "log(address,address,uint256)" [AbiAddressType, AbiAddressType, AbiUIntType 256] + , sig "log(address,address,string)" [AbiAddressType, AbiAddressType, AbiStringType] + , sig "log(address,uint256,bool)" [AbiAddressType, AbiUIntType 256, AbiBoolType] + , sig "log(address,uint256,address)" [AbiAddressType, AbiUIntType 256, AbiAddressType] + , sig "log(address,uint256,uint256)" [AbiAddressType, AbiUIntType 256, AbiUIntType 256] + , sig "log(address,uint256,string)" [AbiAddressType, AbiUIntType 256, AbiStringType] + , sig "log(address,string,bool)" [AbiAddressType, AbiStringType, AbiBoolType] + , sig "log(address,string,address)" [AbiAddressType, AbiStringType, AbiAddressType] + , sig "log(address,string,uint256)" [AbiAddressType, AbiStringType, AbiUIntType 256] + , sig "log(address,string,string)" [AbiAddressType, AbiStringType, AbiStringType] + , sig "log(uint256,bool,bool)" [AbiUIntType 256, AbiBoolType, AbiBoolType] + , sig "log(uint256,bool,address)" [AbiUIntType 256, AbiBoolType, AbiAddressType] + , sig "log(uint256,bool,uint256)" [AbiUIntType 256, AbiBoolType, AbiUIntType 256] + , sig "log(uint256,bool,string)" [AbiUIntType 256, AbiBoolType, AbiStringType] + , sig "log(uint256,address,bool)" [AbiUIntType 256, AbiAddressType, AbiBoolType] + , sig "log(uint256,address,address)" [AbiUIntType 256, AbiAddressType, AbiAddressType] + , sig "log(uint256,address,uint256)" [AbiUIntType 256, AbiAddressType, AbiUIntType 256] + , sig "log(uint256,address,string)" [AbiUIntType 256, AbiAddressType, AbiStringType] + , sig "log(uint256,uint256,bool)" [AbiUIntType 256, AbiUIntType 256, AbiBoolType] + , sig "log(uint256,uint256,address)" [AbiUIntType 256, AbiUIntType 256, AbiAddressType] + , sig "log(uint256,uint256,uint256)" [AbiUIntType 256, AbiUIntType 256, AbiUIntType 256] + , sig "log(uint256,uint256,string)" [AbiUIntType 256, AbiUIntType 256, AbiStringType] + , sig "log(uint256,string,bool)" [AbiUIntType 256, AbiStringType, AbiBoolType] + , sig "log(uint256,string,address)" [AbiUIntType 256, AbiStringType, AbiAddressType] + , sig "log(uint256,string,uint256)" [AbiUIntType 256, AbiStringType, AbiUIntType 256] + , sig "log(uint256,string,string)" [AbiUIntType 256, AbiStringType, AbiStringType] + , sig "log(string,bool,bool)" [AbiStringType, AbiBoolType, AbiBoolType] + , sig "log(string,bool,address)" [AbiStringType, AbiBoolType, AbiAddressType] + , sig "log(string,bool,uint256)" [AbiStringType, AbiBoolType, AbiUIntType 256] + , sig "log(string,bool,string)" [AbiStringType, AbiBoolType, AbiStringType] + , sig "log(string,address,bool)" [AbiStringType, AbiAddressType, AbiBoolType] + , sig "log(string,address,address)" [AbiStringType, AbiAddressType, AbiAddressType] + , sig "log(string,address,uint256)" [AbiStringType, AbiAddressType, AbiUIntType 256] + , sig "log(string,address,string)" [AbiStringType, AbiAddressType, AbiStringType] + , sig "log(string,uint256,bool)" [AbiStringType, AbiUIntType 256, AbiBoolType] + , sig "log(string,uint256,address)" [AbiStringType, AbiUIntType 256, AbiAddressType] + , sig "log(string,uint256,uint256)" [AbiStringType, AbiUIntType 256, AbiUIntType 256] + , sig "log(string,uint256,string)" [AbiStringType, AbiUIntType 256, AbiStringType] + , sig "log(string,string,bool)" [AbiStringType, AbiStringType, AbiBoolType] + , sig "log(string,string,address)" [AbiStringType, AbiStringType, AbiAddressType] + , sig "log(string,string,uint256)" [AbiStringType, AbiStringType, AbiUIntType 256] + , sig "log(string,string,string)" [AbiStringType, AbiStringType, AbiStringType] + -- four params (most common combinations) + , sig "log(bool,bool,bool,bool)" [AbiBoolType, AbiBoolType, AbiBoolType, AbiBoolType] + , sig "log(bool,bool,bool,address)" [AbiBoolType, AbiBoolType, AbiBoolType, AbiAddressType] + , sig "log(bool,bool,bool,uint256)" [AbiBoolType, AbiBoolType, AbiBoolType, AbiUIntType 256] + , sig "log(bool,bool,bool,string)" [AbiBoolType, AbiBoolType, AbiBoolType, AbiStringType] + , sig "log(address,address,address,address)" [AbiAddressType, AbiAddressType, AbiAddressType, AbiAddressType] + , sig "log(address,address,address,uint256)" [AbiAddressType, AbiAddressType, AbiAddressType, AbiUIntType 256] + , sig "log(address,address,address,string)" [AbiAddressType, AbiAddressType, AbiAddressType, AbiStringType] + , sig "log(uint256,uint256,uint256,uint256)" [AbiUIntType 256, AbiUIntType 256, AbiUIntType 256, AbiUIntType 256] + , sig "log(uint256,uint256,uint256,string)" [AbiUIntType 256, AbiUIntType 256, AbiUIntType 256, AbiStringType] + , sig "log(string,string,string,string)" [AbiStringType, AbiStringType, AbiStringType, AbiStringType] + , sig "log(string,string,string,uint256)" [AbiStringType, AbiStringType, AbiStringType, AbiUIntType 256] + , sig "log(string,string,string,address)" [AbiStringType, AbiStringType, AbiStringType, AbiAddressType] + , sig "log(string,string,string,bool)" [AbiStringType, AbiStringType, AbiStringType, AbiBoolType] + , sig "log(string,uint256,uint256,uint256)" [AbiStringType, AbiUIntType 256, AbiUIntType 256, AbiUIntType 256] + , sig "log(string,address,address,address)" [AbiStringType, AbiAddressType, AbiAddressType, AbiAddressType] + , sig "log(string,bool,bool,bool)" [AbiStringType, AbiBoolType, AbiBoolType, AbiBoolType] + , sig "log(string,string,uint256,uint256)" [AbiStringType, AbiStringType, AbiUIntType 256, AbiUIntType 256] + , sig "log(string,uint256,string,uint256)" [AbiStringType, AbiUIntType 256, AbiStringType, AbiUIntType 256] + , sig "log(string,address,uint256,uint256)" [AbiStringType, AbiAddressType, AbiUIntType 256, AbiUIntType 256] + , sig "log(string,uint256,address,uint256)" [AbiStringType, AbiUIntType 256, AbiAddressType, AbiUIntType 256] + , sig "log(string,bool,string,string)" [AbiStringType, AbiBoolType, AbiStringType, AbiStringType] + , sig "log(string,string,address,address)" [AbiStringType, AbiStringType, AbiAddressType, AbiAddressType] + , sig "log(string,address,string,address)" [AbiStringType, AbiAddressType, AbiStringType, AbiAddressType] + , sig "log(string,address,address,string)" [AbiStringType, AbiAddressType, AbiAddressType, AbiStringType] + , sig "log(string,uint256,uint256,string)" [AbiStringType, AbiUIntType 256, AbiUIntType 256, AbiStringType] + , sig "log(string,uint256,string,string)" [AbiStringType, AbiUIntType 256, AbiStringType, AbiStringType] + , sig "log(string,string,uint256,string)" [AbiStringType, AbiStringType, AbiUIntType 256, AbiStringType] + , sig "log(string,address,uint256,string)" [AbiStringType, AbiAddressType, AbiUIntType 256, AbiStringType] + , sig "log(string,uint256,address,string)" [AbiStringType, AbiUIntType 256, AbiAddressType, AbiStringType] + , sig "log(string,address,string,uint256)" [AbiStringType, AbiAddressType, AbiStringType, AbiUIntType 256] + , sig "log(string,string,address,uint256)" [AbiStringType, AbiStringType, AbiAddressType, AbiUIntType 256] + , sig "log(string,address,string,string)" [AbiStringType, AbiAddressType, AbiStringType, AbiStringType] + , sig "log(string,bool,address,address)" [AbiStringType, AbiBoolType, AbiAddressType, AbiAddressType] + , sig "log(string,bool,uint256,uint256)" [AbiStringType, AbiBoolType, AbiUIntType 256, AbiUIntType 256] + , sig "log(string,bool,string,uint256)" [AbiStringType, AbiBoolType, AbiStringType, AbiUIntType 256] + , sig "log(string,bool,uint256,string)" [AbiStringType, AbiBoolType, AbiUIntType 256, AbiStringType] + , sig "log(string,bool,address,string)" [AbiStringType, AbiBoolType, AbiAddressType, AbiStringType] + , sig "log(string,bool,string,address)" [AbiStringType, AbiBoolType, AbiStringType, AbiAddressType] + , sig "log(string,bool,uint256,address)" [AbiStringType, AbiBoolType, AbiUIntType 256, AbiAddressType] + , sig "log(string,bool,address,uint256)" [AbiStringType, AbiBoolType, AbiAddressType, AbiUIntType 256] + , sig "log(string,bool,bool,string)" [AbiStringType, AbiBoolType, AbiBoolType, AbiStringType] + , sig "log(string,bool,bool,address)" [AbiStringType, AbiBoolType, AbiBoolType, AbiAddressType] + , sig "log(string,bool,bool,uint256)" [AbiStringType, AbiBoolType, AbiBoolType, AbiUIntType 256] + ] + where + sig :: ByteString -> [AbiType] -> (FunctionSelector, (Text, [AbiType])) + sig s types = (abiKeccak s, (pack (Char8.unpack s), types)) diff --git a/src/EVM/Format.hs b/src/EVM/Format.hs index 838e2059a..1ca880548 100644 --- a/src/EVM/Format.hs +++ b/src/EVM/Format.hs @@ -39,6 +39,7 @@ import Prelude hiding (LT, GT) import EVM (traceForest, traceForest', traceContext, cheatCode) import EVM.ABI (getAbiSeq, parseTypeName, AbiValue(..), AbiType(..), SolError(..), Indexed(..), Event(..)) +import EVM.ConsoleLog (formatConsoleLog) import EVM.Dapp (DappContext(..), DappInfo(..), findSrc, showTraceLocation) import EVM.Expr qualified as Expr import EVM.Solidity (SolcContract(..), Method(..)) @@ -328,6 +329,8 @@ showTrace trace = ReturnTrace out (CreationContext {}) -> let l = Expr.bufLength out in "← " <> formatExpr l <> " bytes of code" + ConsoleLog buf -> + "\x1b[36m" <> formatConsoleLog buf <> "\x1b[0m" EntryTrace t -> t FrameTrace (CreationContext { address }) -> diff --git a/src/EVM/Types.hs b/src/EVM/Types.hs index 3b35c9d96..dd6129982 100644 --- a/src/EVM/Types.hs +++ b/src/EVM/Types.hs @@ -974,6 +974,7 @@ data TraceData | ErrorTrace EvmError | EntryTrace Text | ReturnTrace (Expr Buf) FrameContext + | ConsoleLog (Expr Buf) deriving (Eq, Ord, Show, Generic) -- | Wrapper type containing vm traces and the context needed to pretty print them properly diff --git a/test/EVM/ConcreteExecution/ConcreteExecutionTests.hs b/test/EVM/ConcreteExecution/ConcreteExecutionTests.hs index 28c2d4011..9682f42f9 100644 --- a/test/EVM/ConcreteExecution/ConcreteExecutionTests.hs +++ b/test/EVM/ConcreteExecution/ConcreteExecutionTests.hs @@ -10,6 +10,7 @@ import Data.Maybe (fromMaybe) import Data.String.Here import Data.Text (Text) import Data.Text qualified as T +import Data.Tree (flatten) import Data.Vector qualified as V import EVM.ABI @@ -20,7 +21,7 @@ import EVM.Solvers (defMemLimit) import EVM.Stepper qualified import EVM.Transaction (initTx) import EVM.Types -import EVM (initialContract, makeVm, defaultVMOpts) +import EVM (initialContract, makeVm, defaultVMOpts, traceForest) import Test.Tasty import Test.Tasty.HUnit @@ -35,8 +36,9 @@ compileOneContract sourceCode contractName = solcRuntime contractName sourceCode <&> fromMaybe (internalError $ "Contract " <> (show contractName) <> " not present in the given source code") -executeSingleCall :: SourceCode -> ContractName -> FunctionName -> Args -> IO ExecResult -executeSingleCall sourceCode contractName functionName abiArgs = do +-- | Set up and run a single contract call, returning the final VM state. +setupAndRunCall :: SourceCode -> ContractName -> FunctionName -> Args -> EVM.Stepper.Stepper Concrete a -> IO a +setupAndRunCall sourceCode contractName functionName abiArgs stepper = do runtimeCode <- compileOneContract sourceCode contractName let functionSignature = functionName <> "(" <> T.intercalate "," (map (abiTypeSolidity . abiValueType) abiArgs) <> ")" let callData = abiMethod functionSignature (AbiTuple $ V.fromList abiArgs) @@ -47,7 +49,13 @@ executeSingleCall sourceCode contractName functionName abiArgs = do } let withInitializedTransactionVM = EVM.Transaction.initTx initialVM let fetcher = Fetch.zero 0 Nothing defMemLimit - Effects.runApp $ EVM.Stepper.interpret fetcher withInitializedTransactionVM EVM.Stepper.execFully + Effects.runApp $ EVM.Stepper.interpret fetcher withInitializedTransactionVM stepper + +executeSingleCall :: SourceCode -> ContractName -> FunctionName -> Args -> IO ExecResult +executeSingleCall src name func args = setupAndRunCall src name func args EVM.Stepper.execFully + +executeSingleCallVM :: SourceCode -> ContractName -> FunctionName -> Args -> IO (VM Concrete) +executeSingleCallVM src name func args = setupAndRunCall src name func args EVM.Stepper.runFully tests :: TestTree tests = testGroup "Concrete execution tests" @@ -56,6 +64,30 @@ tests = testGroup "Concrete execution tests" , testCase "revert-on-arithmetic-overflow" $ expectRevert =<< executeSingleCall simpleCheckedArithmeticExample "C" "a" [AbiUInt 8 250] , testCase "cheat-assume-satisfied" $ expectValue (AbiUInt 8 42) =<< executeSingleCall assumeCheatCodeExample "C" "a" [AbiUInt 8 42] , testCase "cheat-assume-failing" $ expectError AssumeCheatFailed =<< executeSingleCall assumeCheatCodeExample "C" "a" [AbiUInt 8 5] + , testCase "console-log-does-not-revert" $ do + res <- executeSingleCall consoleLogExample "C" "a" [AbiUInt 256 42] + _ <- expectSuccess res + pure () + , testCase "console-log-produces-trace" $ do + vm <- executeSingleCallVM consoleLogExample "C" "a" [AbiUInt 256 42] + let traces = concatMap flatten (traceForest vm) + hasConsoleLog = any (\t -> case t.tracedata of ConsoleLog _ -> True; _ -> False) traces + assertBool "Expected a ConsoleLog trace" hasConsoleLog + , testCase "console-log-extcodesize-nonzero" $ do + res <- executeSingleCall consoleLogExtcodesizeExample "C" "a" [] + val <- expectSuccess res + let obtained = decodeAbiValue (AbiUIntType 256) (BS.fromStrict val) + assertEqual "extcodesize of console addr should be nonzero" (AbiUInt 256 1) obtained + , testCase "console-log-returns-correct-value" $ do + res <- executeSingleCall consoleLogExample "C" "a" [AbiUInt 256 99] + val <- expectSuccess res + let obtained = decodeAbiValue (AbiUIntType 256) (BS.fromStrict val) + assertEqual "return value should be x+1" (AbiUInt 256 100) obtained + , testCase "console-log-multiple-calls" $ do + vm <- executeSingleCallVM consoleLogMultipleExample "C" "a" [AbiUInt 256 5] + let traces = concatMap flatten (traceForest vm) + consoleLogs = filter (\t -> case t.tracedata of ConsoleLog _ -> True; _ -> False) traces + assertEqual "Should have 2 console.log traces" 2 (length consoleLogs) ] expectValue :: AbiValue -> ExecResult -> IO () @@ -122,4 +154,45 @@ assumeCheatCodeExample = [here| b = x; } } -|] \ No newline at end of file +|] + +consoleLogExtcodesizeExample :: Text +consoleLogExtcodesizeExample = [here| + contract C { + function a() public view returns (uint256) { + address console = 0x000000000000000000636F6e736F6c652e6c6f67; + uint256 sz; + assembly { sz := extcodesize(console) } + return sz; + } + } +|] + +consoleLogMultipleExample :: Text +consoleLogMultipleExample = [here| + contract C { + function a(uint256 x) public view returns (uint256) { + address console = 0x000000000000000000636F6e736F6c652e6c6f67; + bytes memory p1 = abi.encodeWithSignature("log(string)", "before"); + (bool s1,) = console.staticcall(p1); + require(s1); + bytes memory p2 = abi.encodeWithSignature("log(uint256)", x); + (bool s2,) = console.staticcall(p2); + require(s2); + return x; + } + } +|] + +consoleLogExample :: Text +consoleLogExample = [here| + contract C { + function a(uint256 x) public view returns (uint256) { + address console = 0x000000000000000000636F6e736F6c652e6c6f67; + bytes memory payload = abi.encodeWithSignature("log(string,uint256)", "value is", x); + (bool s,) = console.staticcall(payload); + require(s); + return x + 1; + } + } +|] diff --git a/test/test.hs b/test/test.hs index 7878cd208..901663cc0 100644 --- a/test/test.hs +++ b/test/test.hs @@ -48,6 +48,7 @@ import Optics.State import EVM import EVM.ABI import EVM.Assembler +import EVM.ConsoleLog (formatConsoleLog) import EVM.Exec import EVM.Expr qualified as Expr import EVM.Fetch qualified as Fetch @@ -139,6 +140,33 @@ tests = testGroup "hevm" , SymExecTests.tests , EquivalenceTests.tests , FoundryTests.tests + , testGroup "Console log formatting" + [ testCase "format-string" $ do + let encoded = ConcreteBuf $ abiMethod "log(string)" (AbiTuple $ V.fromList [AbiString "hello world"]) + assertEqual "console.log(string) format" "console::log(\"hello world\")" (formatConsoleLog encoded) + , testCase "format-uint" $ do + let encoded = ConcreteBuf $ abiMethod "log(uint256)" (AbiTuple $ V.fromList [AbiUInt 256 42]) + assertEqual "console.log(uint256) format" "console::log(42)" (formatConsoleLog encoded) + , testCase "format-string-uint" $ do + let encoded = ConcreteBuf $ abiMethod "log(string,uint256)" (AbiTuple $ V.fromList [AbiString "count", AbiUInt 256 7]) + assertEqual "console.log(string,uint256) format" "console::log(\"count\", 7)" (formatConsoleLog encoded) + , testCase "format-bool" $ do + let encoded = ConcreteBuf $ abiMethod "log(bool)" (AbiTuple $ V.fromList [AbiBool True]) + assertEqual "console.log(bool) format" "console::log(true)" (formatConsoleLog encoded) + , testCase "format-address" $ do + let encoded = ConcreteBuf $ abiMethod "log(address)" (AbiTuple $ V.fromList [AbiAddress 0xdeadbeef]) + assertEqual "console.log(address) format" "console::log(0x00000000000000000000000000000000DeaDBeef)" (formatConsoleLog encoded) + , testCase "format-no-args" $ do + let encoded = ConcreteBuf $ abiMethod "log()" (AbiTuple $ V.fromList []) + assertEqual "console.log() format" "console::log()" (formatConsoleLog encoded) + , testCase "format-unknown-selector" $ do + let encoded = ConcreteBuf $ BS.pack [0xde, 0xad, 0xbe, 0xef, 0x01, 0x02] + let result = formatConsoleLog encoded + assertBool "unknown selector should produce hex fallback" (T.isPrefixOf "console::log(0x" result) + , testCase "format-short-input" $ do + let encoded = ConcreteBuf $ BS.pack [0x01, 0x02] + assertEqual "short input format" "console::log()" (formatConsoleLog encoded) + ] , testGroup "StorageTests" [ test "accessStorage uses fetchedStorage" $ do let dummyContract =