Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cheatcodes as per new std-forge library #567

Merged
merged 29 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a16d3c0
Making assert... into reverts, as per std-forge
msooseth Oct 24, 2024
29279b0
Fixing some issues with tests
msooseth Oct 29, 2024
2cb92a9
Temporary
msooseth Oct 30, 2024
92a7048
Making progress
msooseth Oct 31, 2024
0905738
Moving to forge-std
msooseth Oct 31, 2024
da695e3
Fixing up
msooseth Oct 31, 2024
fed5b2a
Better error reporting
msooseth Oct 31, 2024
b00abc9
Update to bubble hevm errors back up
msooseth Nov 8, 2024
0f7eb14
Temporarily ignoring test while we figure out what to do with it
msooseth Nov 25, 2024
ad1e91a
Fixing interpretFailure
msooseth Nov 25, 2024
e155fb7
Ripping out interpretFailure idea
msooseth Nov 25, 2024
c8bd6b7
More debug printing, enabling tests
msooseth Nov 25, 2024
ed08948
Adding notes
msooseth Nov 25, 2024
3797dc0
Much easier to review and more compact thanks to @d-xo
msooseth Nov 26, 2024
65a8103
One more test on mem pairs
msooseth Nov 26, 2024
a6124bb
Fixing up call signature generation
msooseth Nov 26, 2024
a6151dd
Removing warnings
msooseth Nov 26, 2024
753a8f0
Fixing up rpc tests
msooseth Nov 26, 2024
66dbc56
Updating changelog
msooseth Nov 26, 2024
a01a83e
Removing DSTest, replacing with forge-std
msooseth Nov 26, 2024
dab414a
Update changelog
msooseth Nov 26, 2024
a4c6a95
Cleanup of changelog
msooseth Nov 27, 2024
1d55496
This is not needed anymore, it's not encoded there
msooseth Nov 27, 2024
4de8f70
Removing space
msooseth Nov 27, 2024
be2a67c
Merge branch 'main' into implement_new_cheatcodes
msooseth Nov 29, 2024
88ccb41
Allowing backwards compatibility with ds-test
msooseth Dec 2, 2024
8e4dce0
Merge branch 'main' into implement_new_cheatcodes
msooseth Dec 2, 2024
0d07497
Separate option for DSTest-type assertion type
msooseth Dec 2, 2024
f7219b9
Updating changelog
msooseth Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 std-forge library's way of detecting failures, i.e. through
reverting with a specific error code

## 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
Loading