diff --git a/CHANGELOG.md b/CHANGELOG.md index ccfdf87a1..38e30de38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/cli/cli.hs b/cli/cli.hs index 6517e8386..36d3de578 100644 --- a/cli/cli.hs +++ b/cli/cli.hs @@ -77,7 +77,7 @@ 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)" , 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" @@ -147,11 +147,11 @@ 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)" } - | 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)" , 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" diff --git a/doc/src/exec.md b/doc/src/exec.md index ea5db639f..f85d27145 100644 --- a/doc/src/exec.md +++ b/doc/src/exec.md @@ -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 diff --git a/doc/src/symbolic.md b/doc/src/symbolic.md index ed9b74463..f88d6e8b6 100644 --- a/doc/src/symbolic.md +++ b/doc/src/symbolic.md @@ -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) diff --git a/doc/src/test.md b/doc/src/test.md index 44f1b186e..6359b6eb1 100644 --- a/doc/src/test.md +++ b/doc/src/test.md @@ -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 diff --git a/funding.json b/funding.json deleted file mode 100644 index fcef50889..000000000 --- a/funding.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "opRetro": { - "projectId": "0x2c97e213fef2bd3f30a71edf6ed48232640368d0083dc0a134a1b59391639bde" - } -} diff --git a/src/EVM.hs b/src/EVM.hs index a5a83930c..f5888c609 100644 --- a/src/EVM.hs +++ b/src/EVM.hs @@ -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 @@ -1695,7 +1696,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 () @@ -1903,6 +1904,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)) @@ -1942,6 +1987,77 @@ 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) + assertEq abitype sig input = do + case decodeBuf [abitype, abitype] input of + CAbi [a, b] | a == b -> doStop + CAbi [a, b] -> revertErr a b "!=" + SAbi [ew1, ew2] -> case (Expr.simplify (Expr.eq ew1 ew2)) of + Lit 0 -> revertErr ew1 ew2 "!=" + Lit _ -> doStop + ew -> branch ew $ \case + False ->revertErr ew1 ew2 "!=" + True -> doStop + abivals -> vmError (BadCheatCode (paramDecodeErr abitype "assertEq" abivals) sig) + assertNotEq abitype sig input = do + case decodeBuf [abitype, abitype] input of + CAbi [a, b] | a /= b -> doStop + CAbi [a, b] -> revertErr a b "==" + SAbi [ew1, ew2] -> case (Expr.simplify (Expr.eq ew1 ew2)) of + Lit 1 -> revertErr ew1 ew2 "==" + Lit _ -> doStop + ew -> branch ew $ \case + True ->revertErr ew1 ew2 "==" + False -> doStop + abivals -> vmError (BadCheatCode (paramDecodeErr abitype "assertNotEq" abivals) sig) + assertLt abitype sig input = do + case decodeBuf [abitype, abitype] input of + CAbi [a, b] | a < b -> doStop + CAbi [a, b] -> revertErr a b ">=" + SAbi [ew1, ew2] -> case (Expr.simplify (Expr.lt ew1 ew2)) of + Lit 0 -> revertErr ew1 ew2 ">=" + Lit _ -> doStop + ew -> branch ew $ \case + False ->revertErr ew1 ew2 ">=" + True -> doStop + abivals -> vmError (BadCheatCode (paramDecodeErr abitype "assertLt" abivals) sig) + assertGt abitype sig input = + case decodeBuf [abitype, abitype] input of + CAbi [a, b] | a > b -> doStop + CAbi [a, b] -> revertErr a b "<=" + SAbi [ew1, ew2] -> case (Expr.simplify (Expr.gt ew1 ew2)) of + Lit 0 -> revertErr ew1 ew2 "<=" + Lit _ -> doStop + ew -> branch ew $ \case + False ->revertErr ew1 ew2 "<=" + True -> doStop + abivals -> vmError (BadCheatCode (paramDecodeErr abitype "assertGt" abivals) sig) + assertLe abitype sig input = + case decodeBuf [abitype, abitype] input of + CAbi [a, b] | a <= b -> doStop + CAbi [a, b] -> revertErr a b ">" + SAbi [ew1, ew2] -> case (Expr.simplify (Expr.leq ew1 ew2)) of + Lit 0 -> revertErr ew1 ew2 ">" + Lit _ -> doStop + ew -> branch ew $ \case + False ->revertErr ew1 ew2 ">" + True -> doStop + abivals -> vmError (BadCheatCode (paramDecodeErr abitype "assertLe" abivals) sig) + assertGe abitype sig input = + case decodeBuf [abitype, abitype] input of + CAbi [a, b] | a >= b -> doStop + CAbi [a, b] -> revertErr a b "<" + SAbi [ew1, ew2] -> case (Expr.simplify (Expr.geq ew1 ew2)) of + Lit 0 -> revertErr ew1 ew2 "<" + Lit 1 -> doStop + ew -> branch ew $ \case + False ->revertErr ew1 ew2 "<" + True -> doStop + abivals -> vmError (BadCheatCode (paramDecodeErr abitype "assertGe" abivals) sig) + -- * General call implementation ("delegateCall") -- note that the continuation is ignored in the precompile case @@ -2212,14 +2328,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 @@ -2267,11 +2379,13 @@ finishFrame how = do push 0 -- Case 3: Error during a call? - FrameErrored _ -> do + FrameErrored e -> do revertContracts revertSubstate - assign (#state % #returndata) mempty - push 0 + if (isInterpretFailure e) then finishFrame (FrameErrored e) + else do + assign (#state % #returndata) mempty + push 0 -- Or were we creating? CreationContext _ _ reversion subState' -> do creator <- use (#state % #contract) @@ -2314,11 +2428,13 @@ finishFrame how = do push 0 -- Case 6: Error during a creation? - FrameErrored _ -> do + FrameErrored e -> do revertContracts revertSubstate - assign (#state % #returndata) mempty - push 0 + if (isInterpretFailure e) then finishFrame (FrameErrored e) + else do + assign (#state % #returndata) mempty + push 0 -- * Memory helpers diff --git a/src/EVM/ABI.hs b/src/EVM/ABI.hs index 73920f6f4..58d97dcc2 100644 --- a/src/EVM/ABI.hs +++ b/src/EVM/ABI.hs @@ -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 diff --git a/src/EVM/Solidity.hs b/src/EVM/Solidity.hs index dfdb0e8d2..ca9bdf973 100644 --- a/src/EVM/Solidity.hs +++ b/src/EVM/Solidity.hs @@ -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 @@ -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 @@ -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 diff --git a/src/EVM/Types.hs b/src/EVM/Types.hs index d03f2ee94..3b67fc557 100644 --- a/src/EVM/Types.hs +++ b/src/EVM/Types.hs @@ -550,6 +550,46 @@ data EvmError | NonexistentFork Int deriving (Show, Eq, Ord) +isInterpretFailure :: EvmError -> Bool +isInterpretFailure = \case + (BadCheatCode {}) -> True + (NonexistentFork {}) -> True + PrecompileFailure -> True + StateChangeWhileStatic -> True + UnrecognizedOpcode _ -> True + _ -> False + +isInterpretFailEnd :: Expr a -> Bool +isInterpretFailEnd = \case + (Failure _ _ k) -> isInterpretFailure k + _ -> False + +evmErrToString :: EvmError -> String +evmErrToString = \case + -- NOTE: error text made to closely match go-ethereum's errors.go file + OutOfGas {} -> "Out of gas" + -- TODO "contract creation code storage out of gas" not handled + CallDepthLimitReached -> "Max call depth exceeded" + BalanceTooLow {} -> "Insufficient balance for transfer" + -- TODO "contract address collision" not handled + Revert {} -> "Execution reverted" + -- TODO "max initcode size exceeded" not handled + MaxCodeSizeExceeded {} -> "Max code size exceeded" + BadJumpDestination -> "Invalid jump destination" + StateChangeWhileStatic -> "Attempting to modify state while in static context" + ReturnDataOutOfBounds -> "Return data out of bounds" + IllegalOverflow -> "Gas uint64 overflow" + UnrecognizedOpcode op -> "Invalid opcode: 0x" <> showHex op "" + NonceOverflow -> "Nonce uint64 overflow" + StackUnderrun -> "Stack underflow" + StackLimitExceeded -> "Stack limit reached" + InvalidMemoryAccess -> "Write protection" + (BadCheatCode err fun) -> err <> " Cheatcode function selector: " <> show fun + NonexistentFork fork -> "Nonexistent fork: " <> show fork + PrecompileFailure -> "Precompile failure" + err -> "hevm error: " <> show err + + -- | Sometimes we can only partially execute a given program data PartialExec = UnexpectedSymbolicArg { pc :: Int, opcode :: String, msg :: String, args :: [SomeExpr] } diff --git a/src/EVM/UnitTest.hs b/src/EVM/UnitTest.hs index 998629bc3..8b4b7afeb 100644 --- a/src/EVM/UnitTest.hs +++ b/src/EVM/UnitTest.hs @@ -21,7 +21,7 @@ import EVM.Transaction (initTx) import EVM.Stepper (Stepper) import EVM.Stepper qualified as Stepper -import Control.Monad (void, when, forM, forM_) +import Control.Monad (void, when, forM, forM_, unless) import Control.Monad.ST (RealWorld, ST, stToIO) import Control.Monad.State.Strict (execState, get, put, liftIO) import Optics.Core @@ -29,6 +29,7 @@ import Optics.State import Optics.State.Operators import Data.Binary.Get (runGet) import Data.ByteString (ByteString) +import Data.ByteString.Char8 qualified as BS import Data.ByteString.Lazy qualified as BSLazy import Data.Decimal (DecimalRaw(..)) import Data.Foldable (toList) @@ -203,16 +204,18 @@ symRun opts@UnitTestOptions{..} vm (Sig testName types) = do Nothing -> And (readStorage' (Lit 0) (testContract store).storage) (Lit 2) .== Lit 2 postcondition = curry $ case shouldFail of True -> \(_, post) -> case post of - Success _ _ _ store -> failed store - _ -> PBool True + Success _ _ _ store -> failed store + _ -> PBool True False -> \(_, post) -> case post of - Success _ _ _ store -> PNeg (failed store) - Failure _ _ (Revert msg) -> case msg of - ConcreteBuf b -> PBool $ b /= panicMsg 0x01 - b -> b ./= ConcreteBuf (panicMsg 0x01) - Failure _ _ _ -> PBool True - Partial _ _ _ -> PBool True - _ -> internalError "Invalid leaf node" + Success _ _ _ store -> PNeg (failed store) + Failure _ _ (Revert msg) -> case msg of + ConcreteBuf b -> + if (BS.isPrefixOf (selector "Error(string)") b) || b == panicMsg 0x01 then PBool False + else PBool True + b -> b ./= ConcreteBuf (panicMsg 0x01) + Failure _ _ _ -> PBool True + Partial _ _ _ -> PBool True + _ -> internalError "Invalid leaf node" vm' <- Stepper.interpret (Fetch.oracle solvers rpcInfo) vm $ Stepper.evm $ do @@ -224,6 +227,17 @@ symRun opts@UnitTestOptions{..} vm (Sig testName types) = do -- check postconditions against vm (e, results) <- verify solvers (makeVeriOpts opts) (symbolify vm') (Just postcondition) let allReverts = not . (any Expr.isSuccess) . flattenExpr $ e + let interpFails = filter (isInterpretFailEnd) $ flattenExpr e + + conf <- readConfig + when conf.debug $ liftIO $ forM_ (filter Expr.isFailure (flattenExpr e)) $ \case + (Failure _ _ a) -> putStrLn $ " -> debug of func: " <> Text.unpack testName <> " Failure at the end of expr: " <> show a; + _ -> internalError "cannot be, filtered for failure" + unless (null interpFails) $ liftIO $ do + putStrLn $ " \x1b[33mWARNING\x1b[0m: hevm was only able to partially explore the test " <> Text.unpack testName <> " due to: "; + forM_ (interpFails) $ \case + (Failure _ _ f) -> putStrLn $ " -> " <> evmErrToString f + _ -> internalError "unexpected failure" when (any isUnknown results || any isError results) $ liftIO $ do putStrLn $ " \x1b[33mWARNING\x1b[0m: hevm was only able to partially explore the test " <> Text.unpack testName <> " due to: "; forM_ (groupIssues (filter isError results)) $ \(num, str) -> putStrLn $ " " <> show num <> "x -> " <> str @@ -287,14 +301,6 @@ execSymTest UnitTestOptions{ .. } method cd = do -- Try running the test method runExpr -checkSymFailures :: VMOps t => UnitTestOptions RealWorld -> Stepper t RealWorld (VM t RealWorld) -checkSymFailures UnitTestOptions { .. } = do - -- Ask whether any assertions failed - Stepper.evm $ do - popTrace - abiCall testParams (Left ("failed()", emptyAbi)) - Stepper.runFully - indentLines :: Int -> Text -> Text indentLines n s = let p = Text.replicate n " " diff --git a/test/EVM/Test/Tracing.hs b/test/EVM/Test/Tracing.hs index a791c4d7b..7db641265 100644 --- a/test/EVM/Test/Tracing.hs +++ b/test/EVM/Test/Tracing.hs @@ -513,26 +513,7 @@ vmtrace vm = } where readoutError :: Maybe (VMResult t s) -> Maybe String - readoutError (Just (VMFailure e)) = case e of - -- NOTE: error text made to closely match go-ethereum's errors.go file - OutOfGas {} -> Just "out of gas" - -- TODO "contract creation code storage out of gas" not handled - CallDepthLimitReached -> Just "max call depth exceeded" - BalanceTooLow {} -> Just "insufficient balance for transfer" - -- TODO "contract address collision" not handled - Revert {} -> Just "execution reverted" - -- TODO "max initcode size exceeded" not handled - MaxCodeSizeExceeded {} -> Just "max code size exceeded" - BadJumpDestination -> Just "invalid jump destination" - StateChangeWhileStatic -> Just "write protection" - ReturnDataOutOfBounds -> Just "return data out of bounds" - IllegalOverflow -> Just "gas uint64 overflow" - UnrecognizedOpcode op -> Just $ "invalid opcode: 0x" <> showHex op "" - NonceOverflow -> Just "nonce uint64 overflow" - StackUnderrun -> Just "stack underflow" - StackLimitExceeded -> Just "stack limit reached" - InvalidMemoryAccess -> Just "write protection" - err -> Just $ "HEVM error: " <> show err + readoutError (Just (VMFailure e)) = Just $ evmErrToString e readoutError _ = Nothing vmres :: VM Concrete s -> VMTraceResult diff --git a/test/EVM/Test/Utils.hs b/test/EVM/Test/Utils.hs index 93c89913f..e44b75534 100644 --- a/test/EVM/Test/Utils.hs +++ b/test/EVM/Test/Utils.hs @@ -79,24 +79,16 @@ callProcessCwd cmd args cwd = do exit_code <- withCreateProcess (proc cmd args) { cwd = Just cwd, delegate_ctlc = True } $ \_ _ _ p -> waitForProcess p case exit_code of - ExitSuccess -> return () + ExitSuccess -> pure () ExitFailure r -> processFailedException "callProcess" cmd args r compile :: App m => ProjectType -> FilePath -> FilePath -> m (Either String BuildOutput) -compile DappTools root src = do - json <- liftIO $ compileWithDSTest src - liftIO $ createDirectory (root "out") - liftIO $ T.writeFile (root "out" "dapp.sol.json") json - readBuildOutput root DappTools -compile CombinedJSON _root _src = error "unsupported" -compile foundryType root src = do +compile CombinedJSON _root _src = internalError "unsupported compile type: CombinedJSON" +compile Foundry root src = do liftIO $ createDirectory (root "src") liftIO $ writeFile (root "src" "unit-tests.t.sol") =<< readFile =<< Paths.getDataFileName src - liftIO $ initLib (root "lib" "ds-test") ("test" "contracts" "lib" "test.sol") "test.sol" liftIO $ initLib (root "lib" "tokens") ("test" "contracts" "lib" "erc20.sol") "erc20.sol" - case foundryType of - FoundryStdLib -> liftIO $ initStdForgeDir (root "lib" "forge-std") - Foundry -> pure () + liftIO $ initStdForgeDir (root "lib" "forge-std") (res,out,err) <- liftIO $ readProcessWithExitCode "forge" ["build", "--ast", "--root", root] "" case res of ExitFailure _ -> pure . Left $ "compilation failed: " <> "exit code: " <> show res <> "\n\nstdout:\n" <> out <> "\n\nstderr:\n" <> err diff --git a/test/contracts/fail/check-prefix.sol b/test/contracts/fail/check-prefix.sol index 6e41a400b..9c1c90db2 100644 --- a/test/contracts/fail/check-prefix.sol +++ b/test/contracts/fail/check-prefix.sol @@ -1,6 +1,6 @@ -import "ds-test/test.sol"; +import "forge-std/Test.sol"; -contract SolidityTest is DSTest { +contract SolidityTest is Test { function setUp() public { } diff --git a/test/contracts/fail/dsProveFail.sol b/test/contracts/fail/dsProveFail.sol index e6ee6f607..f151a7f7c 100644 --- a/test/contracts/fail/dsProveFail.sol +++ b/test/contracts/fail/dsProveFail.sol @@ -1,7 +1,7 @@ -import "ds-test/test.sol"; +import "forge-std/Test.sol"; import "tokens/erc20.sol"; -contract SolidityTest is DSTest { +contract SolidityTest is Test { ERC20 token; function setUp() public { diff --git a/test/contracts/fail/trivial.sol b/test/contracts/fail/trivial.sol index f8e2bb2a4..98736202e 100644 --- a/test/contracts/fail/trivial.sol +++ b/test/contracts/fail/trivial.sol @@ -1,7 +1,7 @@ -import {DSTest} from "ds-test/test.sol"; +import {Test} from "forge-std/Test.sol"; // should run and pass -contract Trivial is DSTest { +contract Trivial is Test { function prove_false() public { assertTrue(false); } diff --git a/test/contracts/pass/abstract.sol b/test/contracts/pass/abstract.sol index cf3114a3b..ee2848e47 100644 --- a/test/contracts/pass/abstract.sol +++ b/test/contracts/pass/abstract.sol @@ -1,7 +1,7 @@ -import {DSTest} from "ds-test/test.sol"; +import {Test} from "forge-std/Test.sol"; // should not be run (no code) -abstract contract MyTest is DSTest { +abstract contract MyTest is Test { function testAbstract() public { assertTrue(true); } diff --git a/test/contracts/pass/cheatCodes.sol b/test/contracts/pass/cheatCodes.sol index a7a5981d2..ba246f1d7 100644 --- a/test/contracts/pass/cheatCodes.sol +++ b/test/contracts/pass/cheatCodes.sol @@ -1,6 +1,6 @@ pragma experimental ABIEncoderV2; -import "ds-test/test.sol"; +import "forge-std/Test.sol"; interface Hevm { function warp(uint256) external; @@ -50,8 +50,10 @@ contract Payable { contract Empty {} -contract CheatCodes is DSTest { +contract CheatCodes is Test { address store = address(new HasStorage()); + address constant HEVM_ADDRESS = + address(bytes20(uint160(uint256(keccak256('hevm cheat code'))))); Hevm hevm = Hevm(HEVM_ADDRESS); function prove_warp_concrete() public { diff --git a/test/contracts/pass/cheatCodesFork.sol b/test/contracts/pass/cheatCodesFork.sol index 2f716070b..f262e83d0 100644 --- a/test/contracts/pass/cheatCodesFork.sol +++ b/test/contracts/pass/cheatCodesFork.sol @@ -1,6 +1,6 @@ pragma experimental ABIEncoderV2; -import "ds-test/test.sol"; +import "forge-std/Test.sol"; interface Hevm { function warp(uint256) external; @@ -28,7 +28,9 @@ contract TestState { } /// @dev This contract's state should be persistent across forks, because it's the contract calling `selectFork` -contract CheatCodesForkDeployee is DSTest { +contract CheatCodesForkDeployee is Test { + address constant HEVM_ADDRESS = + address(bytes20(uint160(uint256(keccak256('hevm cheat code'))))); Hevm hevm = Hevm(HEVM_ADDRESS); address stateContract; uint256 forkId1; @@ -91,7 +93,7 @@ contract CheatCodesForkDeployee is DSTest { /// @dev This contract's state should be persistent across forks, because it's the `msg.sender` when running `deployee_prove_ForkedState`. /// We need this "deployer/deployee" architecture so that `msg.sender` will be concrete when running `deployee_prove_ForkedState`. /// If we were to only use the `CheatCodesForkDeployee` contract, the `msg.sender` would be abstract. -contract CheatCodesFork is DSTest { +contract CheatCodesFork is Test { CheatCodesForkDeployee testContract = new CheatCodesForkDeployee(); function prove_ForkedState() external { testContract.deployee_prove_ForkedState(); diff --git a/test/contracts/pass/constantinople.sol b/test/contracts/pass/constantinople.sol index 4655bd57c..10c5b9c85 100644 --- a/test/contracts/pass/constantinople.sol +++ b/test/contracts/pass/constantinople.sol @@ -1,10 +1,10 @@ -import "ds-test/test.sol"; +import "forge-std/Test.sol"; contract DeadCode{ function dummy() external returns (uint256) {} } -contract ConstantinopleTests is DSTest { +contract ConstantinopleTests is Test { DeadCode notmuch; function setUp() public { notmuch = new DeadCode(); diff --git a/test/contracts/pass/constantinople_min.sol b/test/contracts/pass/constantinople_min.sol index 1c5bbc7b1..01e28c6ae 100644 --- a/test/contracts/pass/constantinople_min.sol +++ b/test/contracts/pass/constantinople_min.sol @@ -1,10 +1,10 @@ -import "ds-test/test.sol"; +import "forge-std/Test.sol"; contract DeadCode{ function dummy() external returns (uint256) {} } -contract ConstantinopleTests is DSTest { +contract ConstantinopleTests is Test { DeadCode notmuch; function setUp() public { notmuch = new DeadCode(); diff --git a/test/contracts/pass/dsProvePass.sol b/test/contracts/pass/dsProvePass.sol index da9a60df4..f8321a7a0 100644 --- a/test/contracts/pass/dsProvePass.sol +++ b/test/contracts/pass/dsProvePass.sol @@ -1,4 +1,4 @@ -import "ds-test/test.sol"; +import "forge-std/Test.sol"; import "tokens/erc20.sol"; contract ConstructorArg { @@ -8,7 +8,7 @@ contract ConstructorArg { } } -contract SolidityTest is DSTest { +contract SolidityTest is Test { ERC20 token; function setUp() public { diff --git a/test/contracts/pass/loops.sol b/test/contracts/pass/loops.sol index 62ed91c88..7d8e4e001 100644 --- a/test/contracts/pass/loops.sol +++ b/test/contracts/pass/loops.sol @@ -1,6 +1,6 @@ -import "ds-test/test.sol"; +import "forge-std/Test.sol"; -contract Loops is DSTest { +contract Loops is Test { function prove_loop(uint n) public { uint counter = 0; diff --git a/test/contracts/pass/transfer.sol b/test/contracts/pass/transfer.sol index 884ec85d5..b87bbdee2 100644 --- a/test/contracts/pass/transfer.sol +++ b/test/contracts/pass/transfer.sol @@ -1,9 +1,9 @@ pragma solidity ^0.8.19; -import "ds-test/test.sol"; +import "forge-std/Test.sol"; import {ERC20} from "tokens/erc20.sol"; -contract SolidityTestFail2 is DSTest { +contract SolidityTestFail2 is Test { ERC20 token; function setUp() public { @@ -11,6 +11,7 @@ contract SolidityTestFail2 is DSTest { } function prove_transfer(uint supply, address usr, uint amt) public { + // require(supply >= amt, "supply must be greater than or equal to amt"); token.mint(address(this), supply); uint prebal = token.balanceOf(usr); @@ -20,7 +21,7 @@ contract SolidityTestFail2 is DSTest { uint expected = usr == address(this) ? 0 // self transfer is a noop : amt; // otherwise `amt` has been transferred to `usr` - assert(expected == postbal - prebal); + assertTrue(expected == postbal - prebal); } } diff --git a/test/contracts/pass/trivial.sol b/test/contracts/pass/trivial.sol index 93feaa643..d18c95820 100644 --- a/test/contracts/pass/trivial.sol +++ b/test/contracts/pass/trivial.sol @@ -1,7 +1,7 @@ -import {DSTest} from "ds-test/test.sol"; +import {Test} from "forge-std/Test.sol"; // should run and pass -contract Trivial is DSTest { +contract Trivial is Test { function prove_true() public { assertTrue(true); } diff --git a/test/contracts/pass/unwind.sol b/test/contracts/pass/unwind.sol index 683daf36f..b528f0cb4 100644 --- a/test/contracts/pass/unwind.sol +++ b/test/contracts/pass/unwind.sol @@ -1,7 +1,7 @@ -import {DSTest} from "ds-test/test.sol"; +import {Test} from "forge-std/Test.sol"; // tests unwind support in precompiles -contract Unwind is DSTest { +contract Unwind is Test { function prove_invalid_sum() public { bytes32 x = hex"01"; try this.callBn256Add(x, x, x, x) returns (bytes32[2] memory) { diff --git a/test/test.hs b/test/test.hs index b428670cd..c49d46cfc 100644 --- a/test/test.hs +++ b/test/test.hs @@ -1436,8 +1436,8 @@ tests = testGroup "hevm" [ test "Trivial-Pass" $ do let testFile = "test/contracts/pass/trivial.sol" runSolidityTest testFile ".*" >>= assertEqualM "test result" True - , test "DappTools" $ do - -- quick smokecheck to make sure that we can parse dapptools style build outputs + , test "Foundry" $ do + -- quick smokecheck to make sure that we can parse ForgeStdLib style build outputs let cases = [ ("test/contracts/pass/trivial.sol", ".*", True) , ("test/contracts/pass/dsProvePass.sol", "proveEasy", True) @@ -1445,7 +1445,7 @@ tests = testGroup "hevm" , ("test/contracts/fail/dsProveFail.sol", "prove_add", False) ] results <- forM cases $ \(testFile, match, expected) -> do - actual <- runSolidityTestCustom testFile match Nothing Nothing False Nothing DappTools + actual <- runSolidityTestCustom testFile match Nothing Nothing False Nothing Foundry pure (actual == expected) assertBoolM "test result" (and results) , test "Trivial-Fail" $ do @@ -1471,7 +1471,7 @@ tests = testGroup "hevm" runSolidityTest testFile "prove_transfer" >>= assertEqualM "should prove transfer" True , test "badvault-sym-branch" $ do let testFile = "test/contracts/fail/10_BadVault.sol" - runSolidityTestCustom testFile "prove_BadVault_usingExploitLaunchPad" Nothing Nothing True Nothing FoundryStdLib >>= assertEqualM "Must find counterexample" False + runSolidityTestCustom testFile "prove_BadVault_usingExploitLaunchPad" Nothing Nothing True Nothing Foundry >>= assertEqualM "Must find counterexample" False , test "Prove-Tests-Fail" $ do let testFile = "test/contracts/fail/dsProveFail.sol" runSolidityTest testFile "prove_trivial" >>= assertEqualM "test result" False @@ -1479,11 +1479,10 @@ tests = testGroup "hevm" runSolidityTest testFile "prove_add" >>= assertEqualM "test result" False runSolidityTestCustom testFile "prove_smtTimeout" (Just 1) Nothing False Nothing Foundry >>= assertEqualM "test result" False runSolidityTest testFile "prove_multi" >>= assertEqualM "test result" False - -- TODO: implement overflow checking optimizations and enable, currently this runs forever - --runSolidityTest testFile "prove_distributivity" >>= assertEqualM "test result" False + runSolidityTest testFile "prove_distributivity" >>= assertEqualM "test result" False , test "Loop-Tests" $ do let testFile = "test/contracts/pass/loops.sol" - runSolidityTestCustom testFile "prove_loop" Nothing (Just 10) False Nothing Foundry >>= assertEqualM "test result" True + runSolidityTestCustom testFile "prove_loop" Nothing (Just 10) False Nothing Foundry >>= assertEqualM "test result" True runSolidityTestCustom testFile "prove_loop" Nothing (Just 100) False Nothing Foundry >>= assertEqualM "test result" False , test "Cheat-Codes-Pass" $ do let testFile = "test/contracts/pass/cheatCodes.sol"