Skip to content

Commit

Permalink
Merge pull request #567 from ethereum/implement_new_cheatcodes
Browse files Browse the repository at this point in the history
Cheatcodes as per new std-forge library
  • Loading branch information
msooseth authored Dec 2, 2024
2 parents ee906ef + f7219b9 commit bc76001
Show file tree
Hide file tree
Showing 33 changed files with 243 additions and 229 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- More complete and precise array/mapping slot rewrite, along with a copySlice improvement
- Use a let expression in copySlice to decrease expression size
- The `--debug` flag now dumps the internal expressions as well
- hevm now uses the forge-std library's way of detecting failures, i.e. through
reverting with a specific error code unless --assertion-type DSTest is passed

## Added
- More POr and PAnd rules
Expand Down Expand Up @@ -41,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
partial results instead of erroring out
- Fix interpreter's MCOPY handling so that it doesn't error out on symbolic arguments
- More desciptive errors in case of a cheatcode issue
- Better and more pretty debug messages

## Fixed
- `concat` is a 2-ary, not an n-ary function in SMT2LIB, declare-const does not exist in QF_AUFBV, replacing
Expand All @@ -63,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Respect --smt-timeout in equivalence checking
- Fixed the handling of returndata with an abstract size during transaction finalization
- Error handling for user-facing cli commands is much improved
- Fixed call signature generation for test cases
- Fixing prank so it doesn't override the sender address on lower call frames

## [0.53.0] - 2024-02-23
Expand Down
15 changes: 11 additions & 4 deletions cli/cli.hs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ import EVM.Types qualified
import EVM.UnitTest
import EVM.Effects

data AssertionType = DSTest | Forge
deriving (Eq, Show, Read, ParseField)

-- This record defines the program's command-line options
-- automatically via the `optparse-generic` package.
data Command w
Expand Down Expand Up @@ -77,7 +80,8 @@ data Command w

-- symbolic execution opts
, root :: w ::: Maybe String <?> "Path to project root directory (default: . )"
, projectType :: w ::: Maybe ProjectType <?> "Is this a Foundry or DappTools project (default: Foundry)"
, projectType :: w ::: Maybe ProjectType <?> "Is this a CombinedJSON or Foundry project (default: Foundry)"
, assertionType :: w ::: Maybe AssertionType <?> "Assertions as per Forge or DSTest (default: Forge)"
, initialStorage :: w ::: Maybe (InitialStorage) <?> "Starting state for storage: Empty, Abstract (default Abstract)"
, sig :: w ::: Maybe Text <?> "Signature of types to decode / encode"
, arg :: w ::: [String] <?> "Values to encode"
Expand Down Expand Up @@ -147,11 +151,13 @@ data Command w
, rpc :: w ::: Maybe URL <?> "Fetch state from a remote node"
, block :: w ::: Maybe W256 <?> "Block state is be fetched from"
, root :: w ::: Maybe String <?> "Path to project root directory (default: . )"
, projectType :: w ::: Maybe ProjectType <?> "Is this a Foundry or DappTools project (default: Foundry)"
, projectType :: w ::: Maybe ProjectType <?> "Is this a CombinedJSON or Foundry project (default: Foundry)"
, assertionType :: w ::: Maybe AssertionType <?> "Assertions as per Forge or DSTest (default: Forge)"
}
| Test -- Run DSTest unit tests
| Test -- Run Foundry unit tests
{ root :: w ::: Maybe String <?> "Path to project root directory (default: . )"
, projectType :: w ::: Maybe ProjectType <?> "Is this a Foundry or DappTools project (default: Foundry)"
, projectType :: w ::: Maybe ProjectType <?> "Is this a CombinedJSON or Foundry project (default: Foundry)"
, assertionType :: w ::: Maybe AssertionType <?> "Assertions as per Forge or DSTest (default: Forge)"
, rpc :: w ::: Maybe URL <?> "Fetch state from a remote node"
, number :: w ::: Maybe W256 <?> "Block: number"
, verbose :: w ::: Maybe Int <?> "Append call trace: {1} failures {2} all"
Expand Down Expand Up @@ -674,6 +680,7 @@ unitTestOptions cmd solvers buildOutput = do
, testParams = params
, dapp = srcInfo
, ffiAllowed = cmd.ffi
, checkFailBit = (fromMaybe Forge cmd.assertionType) == DSTest
}
parseInitialStorage :: InitialStorage -> BaseState
parseInitialStorage Empty = EmptyBase
Expand Down
6 changes: 2 additions & 4 deletions doc/src/exec.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,11 @@ Available options:
--rpc TEXT Fetch state from a remote node
--block W256 Block state is be fetched from
--root STRING Path to project root directory (default: . )
--project-type PROJECTTYPE
Is this a Foundry or DappTools project (default:
Foundry)
--project-type PROJECTTYPE Foundry or CombinedJSON project
```

Minimum required flags: either you must provide `--code` or you must both pass
`--rpc` and `--address`.
`--rpc` and `--address`.

If the execution returns an output, it will be written
to stdout. Exit code indicates whether the execution was successful or
Expand Down
8 changes: 4 additions & 4 deletions doc/src/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ be larger than or equal to 100. Let's see the contract and its associated check:

```solidity
pragma solidity ^0.8.19;
import "ds-test/test.sol";
import "foge-std/Test.sol";
contract MyContract is DSTest {
contract MyContract is Test {
mapping (address => uint) balances;
function prove_add_value(address recv, uint amt) public {
require(balances[recv] < 100);
Expand Down Expand Up @@ -65,9 +65,9 @@ $ chmod +x ./hevm-x86_64-linux
$ forge init .
$ cat <<EOF > src/contract.sol
pragma solidity ^0.8.19;
import "ds-test/test.sol";
import "forge-std/Test.sol";
contract MyContract is DSTest {
contract MyContract is Test {
mapping (address => uint) balances;
function prove_add_value(address recv, uint amt) public {
require(balances[recv] < 100);
Expand Down
4 changes: 1 addition & 3 deletions doc/src/symbolic.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ Available options:
--rpc TEXT Fetch state from a remote node
--block W256 Block state is be fetched from
--root STRING Path to project root directory (default: . )
--project-type PROJECTTYPE
Is this a Foundry or DappTools project (default:
Foundry)
--project-type PROJECTTYPE Foundry or CombinedJSON project
--initial-storage INITIALSTORAGE
Starting state for storage: Empty, Abstract (default
Abstract)
Expand Down
4 changes: 1 addition & 3 deletions doc/src/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ Usage: hevm test [--root STRING] [--project-type PROJECTTYPE] [--rpc TEXT]
Available options:
-h,--help Show this help text
--root STRING Path to project root directory (default: . )
--project-type PROJECTTYPE
Is this a Foundry or DappTools project (default:
Foundry)
--project-type PROJECTTYPE Foundry or CombinedJSON project
--rpc TEXT Fetch state from a remote node
--number W256 Block: number
--verbose INT Append call trace: {1} failures {2} all
Expand Down
8 changes: 4 additions & 4 deletions doc/src/when-to-use.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ bytecode, postconditions need to be explicit. Let's see an example:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "ds-test/test.sol";
import "forge-std/Test.sol";
contract MyContract is DSTest {
contract MyContract is Test {
uint balance;
function test_overflow(uint amt) public {
unchecked {
Expand All @@ -50,9 +50,9 @@ towards them. Let's see a simple one:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "ds-test/test.sol";
import "foge-std/Test.sol";
contract MyContract is DSTest {
contract MyContract is Test {
uint balance;
function prove_multiply(uint amt, uint amt2) public {
require(amt != 1);
Expand Down
5 changes: 0 additions & 5 deletions funding.json

This file was deleted.

81 changes: 72 additions & 9 deletions src/EVM.hs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import Data.Bits (FiniteBits, countLeadingZeros, finiteBitSize)
import Data.ByteArray qualified as BA
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Char8 qualified as BS8
import Data.ByteString.Base16 qualified as BS16
import Data.ByteString.Lazy (fromStrict)
import Data.ByteString.Lazy qualified as LS
Expand Down Expand Up @@ -1688,7 +1689,7 @@ cheat gas (inOffset, inSize) (outOffset, outSize) xs = do
Nothing -> partial $ UnexpectedSymbolicArg vm.state.pc (getOpName vm.state) "symbolic cheatcode selector" (wrap [abi])
Just (unsafeInto -> abi') ->
case Map.lookup abi' cheatActions of
Nothing -> vmError (BadCheatCode "cannot understand cheatcode, maybe cheatcode not supported?" abi')
Nothing -> vmError (BadCheatCode "Cannot understand cheatcode." abi')
Just action -> action input

type CheatAction t s = Expr Buf -> EVM t s ()
Expand Down Expand Up @@ -1897,6 +1898,50 @@ cheatActions = Map.fromList
, $(envReadMultipleCheat "envBytes32(string,string)" $ AbiBytesType 32) stringToBytes32
, $(envReadMultipleCheat "envString(string,string)" AbiStringType) stringToByteString
, $(envReadMultipleCheat "envBytes(bytes,bytes)" AbiBytesDynamicType) stringHexToByteString
, action "assertTrue(bool)" $ \sig input ->
case decodeBuf [AbiBoolType] input of
CAbi [AbiBool True] -> doStop
CAbi [AbiBool False] -> frameRevert "assertion failed"
SAbi [eword] -> case (Expr.simplify (Expr.iszero eword)) of
Lit 1 -> frameRevert "assertion failed"
Lit 0 -> doStop
ew -> branch ew $ \case
True -> frameRevert "assertion failed"
False -> doStop
k -> vmError $ BadCheatCode ("assertTrue(bool) parameter decoding failed: " <> show k) sig
, action "assertFalse(bool)" $ \sig input ->
case decodeBuf [AbiBoolType] input of
CAbi [AbiBool False] -> doStop
CAbi [AbiBool True] -> frameRevert "assertion failed"
SAbi [eword] -> case (Expr.simplify (Expr.iszero eword)) of
Lit 0 -> frameRevert "assertion failed"
Lit 1 -> doStop
ew -> branch ew $ \case
False -> frameRevert "assertion failed"
True -> doStop
k -> vmError $ BadCheatCode ("assertFalse(bool) parameter decoding failed: " <> show k) sig
, action "assertEq(bool,bool)" $ assertEq AbiBoolType
, action "assertEq(uint256,uint256)" $ assertEq (AbiUIntType 256)
, action "assertEq(int256,int256)" $ assertEq (AbiIntType 256)
, action "assertEq(address,address)" $ assertEq AbiAddressType
, action "assertEq(bytes32,bytes32)" $ assertEq (AbiBytesType 32)
, action "assertEq(string,string)" $ assertEq (AbiStringType)
--
, action "assertNotEq(bool,bool)" $ assertNotEq AbiBoolType
, action "assertNotEq(uint256,uint256)" $ assertNotEq (AbiUIntType 256)
, action "assertNotEq(int256,int256)" $ assertNotEq (AbiIntType 256)
, action "assertNotEq(address,address)" $ assertNotEq AbiAddressType
, action "assertNotEq(bytes32,bytes32)" $ assertNotEq (AbiBytesType 32)
, action "assertNotEq(string,string)" $ assertNotEq (AbiStringType)
--
, action "assertLt(uint256,uint256)" $ assertLt (AbiUIntType 256)
, action "assertLt(int256,int256)" $ assertLt (AbiIntType 256)
, action "assertLe(uint256,uint256)" $ assertLe (AbiUIntType 256)
, action "assertLe(int256,int256)" $ assertLe (AbiIntType 256)
, action "assertGt(uint256,uint256)" $ assertGt (AbiUIntType 256)
, action "assertGt(int256,int256)" $ assertGt (AbiIntType 256)
, action "assertGe(uint256,uint256)" $ assertGe (AbiUIntType 256)
, action "assertGe(int256,int256)" $ assertGe (AbiIntType 256)
]
where
action s f = (abiKeccak s, f (abiKeccak s))
Expand Down Expand Up @@ -1936,6 +1981,28 @@ cheatActions = Map.fromList
stringToByteString = Right . Char8.pack
stringHexToByteString :: String -> Either ByteString ByteString
stringHexToByteString s = either (const $ Left "invalid bytes value") Right $ BS16.decodeBase16Untyped . Char8.pack . strip0x $ s
paramDecodeErr abitype name abivals = name <> "(" <> (show abitype) <> "," <> (show abitype) <>
") parameter decoding failed. Error: " <> show abivals
revertErr a b comp = frameRevert $ "assertion failed: " <>
BS8.pack (show a) <> " " <> comp <> " " <> BS8.pack (show b)
genAssert comp exprComp invComp name abitype sig input = do
case decodeBuf [abitype, abitype] input of
CAbi [a, b] | a `comp` b -> doStop
CAbi [a, b] -> revertErr a b invComp
SAbi [ew1, ew2] -> case (Expr.simplify (Expr.iszero $ exprComp ew1 ew2)) of
Lit 0 -> doStop
Lit _ -> revertErr ew1 ew2 invComp
ew -> branch ew $ \case
False -> doStop
True -> revertErr ew1 ew2 invComp
abivals -> vmError (BadCheatCode (paramDecodeErr abitype name abivals) sig)
assertEq = genAssert (==) Expr.eq "!=" "assertEq"
assertNotEq = genAssert (/=) (\a b -> Expr.iszero $ Expr.eq a b) "==" "assertNotEq"
assertLt = genAssert (<) Expr.lt ">=" "assertLt"
assertGt = genAssert (>) Expr.gt "<=" "assertGt"
assertLe = genAssert (<=) Expr.leq ">" "assertLe"
assertGe = genAssert (>=) Expr.geq "<" "assertGe"


-- * General call implementation ("delegateCall")
-- note that the continuation is ignored in the precompile case
Expand Down Expand Up @@ -2209,14 +2276,10 @@ finishFrame how = do
nextFrame : remainingFrames -> do

-- Insert a debug trace.
insertTrace $
case how of
FrameErrored e ->
ErrorTrace e
FrameReverted e ->
ErrorTrace (Revert e)
FrameReturned output ->
ReturnTrace output nextFrame.context
insertTrace $ case how of
FrameReturned output -> ReturnTrace output nextFrame.context
FrameReverted e -> ErrorTrace (Revert e)
FrameErrored e -> ErrorTrace e
-- Pop to the previous level of the debug trace stack.
popTrace

Expand Down
1 change: 0 additions & 1 deletion src/EVM/ABI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,6 @@ decodeBuf tps buf =
else case runGetOrFail (getAbiSeq (length tps) tps) (BSLazy.fromStrict asBS) of
Right ("", _, args) -> CAbi (toList args)
_ -> NoVals

where
isDynamic t = abiKind t == Dynamic

Expand Down
2 changes: 1 addition & 1 deletion src/EVM/Dapp.hs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ mkSig method
| "check" `isPrefixOf` testname = Just (Sig testname argtypes)
| otherwise = Nothing
where
testname = method.methodSignature
testname = method.name
argtypes = snd <$> method.inputs

findUnitTests :: Text -> ([SolcContract] -> [(Text, [Sig])])
Expand Down
10 changes: 1 addition & 9 deletions src/EVM/Solidity.hs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ instance Monoid BuildOutput where
mempty = BuildOutput mempty mempty

-- | The various project types understood by hevm
data ProjectType = DappTools | CombinedJSON | Foundry | FoundryStdLib
data ProjectType = CombinedJSON | Foundry
deriving (Eq, Show, Read, ParseField)

data SourceCache = SourceCache
Expand Down Expand Up @@ -285,13 +285,6 @@ makeSrcMaps = (\case (_, Fe, _) -> Nothing; x -> Just (done x))

-- | Reads all solc output json files found under the provided filepath and returns them merged into a BuildOutput
readBuildOutput :: App m => FilePath -> ProjectType -> m (Either String BuildOutput)
readBuildOutput root DappTools = do
let outDir = root </> "out"
jsons <- liftIO $ findJsonFiles outDir
case jsons of
[x] -> readSolc DappTools root (outDir </> x)
[] -> pure . Left $ "no json files found in: " <> outDir
_ -> pure . Left $ "multiple json files found in: " <> outDir
readBuildOutput root CombinedJSON = do
let outDir = root </> "out"
jsons <- liftIO $ findJsonFiles outDir
Expand Down Expand Up @@ -413,7 +406,6 @@ force :: String -> Maybe a -> a
force s = fromMaybe (internalError s)

readJSON :: ProjectType -> Text -> Text -> Maybe (Contracts, Asts, Sources)
readJSON DappTools _ json = readStdJSON json
readJSON CombinedJSON _ json = readCombinedJSON json
readJSON _ contractName json = readFoundryJSON contractName json

Expand Down
4 changes: 2 additions & 2 deletions src/EVM/Solvers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,10 @@ withSolvers solver count threads timeout cont = do
when (conf.dumpQueries) $ writeSMT2File smt2 fileCounter "abstracted"
if (isJust fuzzResult)
then do
when (conf.debug) $ putStrLn $ "Cex found via fuzzing:" <> (show fuzzResult)
when (conf.debug) $ putStrLn $ " Cex found via fuzzing:" <> (show fuzzResult)
writeChan r (Sat $ fromJust fuzzResult)
else if not conf.onlyCexFuzz then do
when (conf.debug) $ putStrLn "Fuzzing failed to find a Cex"
when (conf.debug) $ putStrLn " Fuzzing failed to find a Cex"
-- reset solver and send all lines of provided script
out <- sendScript inst (SMT2 ("(reset)" : cmds) mempty mempty ps)
case out of
Expand Down
8 changes: 5 additions & 3 deletions src/EVM/SymExec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -596,16 +596,16 @@ verify
verify solvers opts preState maybepost = do
conf <- readConfig
let call = mconcat ["prefix 0x", getCallPrefix preState.state.calldata]
when conf.debug $ liftIO $ putStrLn $ "Exploring call " <> call
when conf.debug $ liftIO $ putStrLn $ " Exploring call " <> call

exprInter <- interpret (Fetch.oracle solvers opts.rpcInfo) opts.maxIter opts.askSmtIters opts.loopHeuristic preState runExpr
when conf.dumpExprs $ liftIO $ T.writeFile "unsimplified.expr" (formatExpr exprInter)
liftIO $ do
when conf.debug $ putStrLn "Simplifying expression"
when conf.debug $ putStrLn " Simplifying expression"
let expr = if opts.simp then (Expr.simplify exprInter) else exprInter
when conf.dumpExprs $ T.writeFile "simplified.expr" (formatExpr expr)

when conf.debug $ putStrLn $ "Exploration finished, " <> show (Expr.numBranches expr) <> " branch(es) to check in call " <> call
when conf.debug $ putStrLn $ " Exploration finished, " <> show (Expr.numBranches expr) <> " branch(es) to check in call " <> call

let flattened = flattenExpr expr
when (any isPartial flattened) $ do
Expand All @@ -627,8 +627,10 @@ verify solvers opts preState maybepost = do
-- Dispatch the remaining branches to the solver to check for violations
results <- flip mapConcurrently withQueries $ \(query, leaf) -> do
res <- checkSat solvers query
when conf.debug $ putStrLn $ " SMT result: " <> show res
pure (res, leaf)
let cexs = filter (\(res, _) -> not . isUnsat $ res) results
when conf.debug $ putStrLn $ " Found " <> show (length cexs) <> " potential counterexample(s) in call " <> call
pure $ if Prelude.null cexs then (expr, [Qed ()]) else (expr, fmap toVRes cexs)
where
getCallPrefix :: Expr Buf -> String
Expand Down
Loading

0 comments on commit bc76001

Please sign in to comment.