packages feed

hevm-0.58.0: test/EVM/ConcreteExecution/ConcreteExecutionTests.hs

{-# LANGUAGE QuasiQuotes #-}

module EVM.ConcreteExecution.ConcreteExecutionTests (tests) where

import Control.Monad.ST (stToIO)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.Functor ((<&>))
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
import EVM.Effects qualified as Effects
import EVM.Fetch qualified as Fetch
import EVM.Solidity (solcRuntime)
import EVM.Solvers (defMemLimit)
import EVM.Stepper qualified
import EVM.Transaction (initTx)
import EVM.Types
import EVM (initialContract, makeVm, defaultVMOpts, traceForest)
import Test.Tasty
import Test.Tasty.HUnit

type SourceCode = Text
type ContractName = Text
type FunctionName = Text
type Args = [AbiValue]
type ExecResult = Either EvmError (Expr Buf)

compileOneContract :: SourceCode -> ContractName -> IO ByteString
compileOneContract sourceCode contractName =
  solcRuntime contractName sourceCode <&> fromMaybe (internalError $ "Contract " <> (show contractName) <> " not present in the given source code")


-- | 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)
  let contractWithCode = initialContract (RuntimeCode $ ConcreteRuntimeCode runtimeCode)
  initialVM <- stToIO $ makeVm $ defaultVMOpts
    { contract = contractWithCode
    , calldata = (ConcreteBuf callData, [])
    }
  let withInitializedTransactionVM = EVM.Transaction.initTx initialVM
  let fetcher = Fetch.zero 0 Nothing defMemLimit
  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"
  [ testCase "return-updated-input-argument" $ expectValue (AbiUInt 8 3) =<< executeSingleCall simpleUncheckedArithmeticExample "C" "a" [AbiUInt 8 1]
  , testCase "silent-overflow-in-unchecked-arithmetic" $ expectValue (AbiUInt 8 245) =<< executeSingleCall simpleUncheckedArithmeticExample "C" "a" [AbiUInt 8 250]
  , 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 ()
expectValue expectedVal res = do
  let abiType = abiValueType expectedVal
  bs <- expectSuccess res
  let obtainedVal = decodeAbiValue abiType (BS.fromStrict bs)
  assertEqual "Incorrect result of concrete execution" expectedVal obtainedVal

expectSuccess :: ExecResult -> IO ByteString
expectSuccess res = do
  buf <- assertRight res
  case buf of
    ConcreteBuf bs -> pure bs
    _ -> assertFailure $ "Unexpected symbolic buffer: " <> show buf

expectRevert :: ExecResult -> IO ()
expectRevert res = do
  err <- assertLeft res
  case err of
    Revert _ -> pure ()
    _ -> assertFailure "Revert was expected, but got a different EVM error"

expectError :: EvmError -> ExecResult -> IO ()
expectError expectedErr res = do
  err <- assertLeft res
  assertEqual "Incorrect error obtained from concrete execution" expectedErr err

assertLeft :: Either e a -> IO e
assertLeft = \case
  Left e -> pure e
  Right _ -> assertFailure "Left we expected, but got Right"

assertRight :: Show e => Either e a -> IO a
assertRight = \case
  Left e -> assertFailure $ "Unexpected error: " <> show e
  Right x -> pure x

simpleUncheckedArithmeticExample :: Text
simpleUncheckedArithmeticExample = [here|
  contract C {
    function a(uint8 x) public returns (uint8 b) {
      unchecked { b = x*2+1; }
    }
  }
|]

simpleCheckedArithmeticExample :: Text
simpleCheckedArithmeticExample = [here|
  contract C {
    function a(uint8 x) public returns (uint8 b) {
      b = x*2+1;
    }
  }
|]

assumeCheatCodeExample :: Text
assumeCheatCodeExample = [here|
  interface Hevm {function assume(bool) external;}
  contract C {
    function a(uint8 x) public returns (uint8 b) {
      Hevm hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
      hevm.assume(x > 10);
      b = x;
    }
  }
|]

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;
    }
  }
|]