diff --git a/nix/build-dapp-package.nix b/nix/build-dapp-package.nix index d091a46b0..ad9d7063f 100644 --- a/nix/build-dapp-package.nix +++ b/nix/build-dapp-package.nix @@ -23,8 +23,9 @@ in , deps ? [] , solc ? "${pkgs.solc}/bin/solc" , shouldFail ? false - , dappFlags ? "" , doCheck ? true + , dapprc ? "" + , testFlags ? "" , ... }@args: pkgs.stdenv.mkDerivation ( rec { inherit doCheck; @@ -48,6 +49,8 @@ in passthru.libPaths; buildPhase = '' mkdir -p out + export LANG=C.UTF-8 + ln -s ${pkgs.writeText "dapprc" dapprc} ./.dapprc export DAPP_SOLC=${solc} export DAPP_REMAPPINGS="$REMAPPINGS" export DAPP_SRC=$src @@ -59,7 +62,7 @@ in ''; checkPhase = let - cmd = "DAPP_SKIP_BUILD=1 dapp test ${dappFlags}"; + cmd = "DAPP_SKIP_BUILD=1 dapp test ${testFlags}"; in if shouldFail then "${cmd} && exit 1 || echo 0" diff --git a/src/dapp-tests/default.nix b/src/dapp-tests/default.nix index 3c44e1473..e3ae24546 100644 --- a/src/dapp-tests/default.nix +++ b/src/dapp-tests/default.nix @@ -114,11 +114,10 @@ let deps = [ ds-test ds-thing ]; }; - runTest = { dir, shouldFail, name, dappFlags?"" }: pkgs.buildDappPackage { - inherit name shouldFail; + runTest = { dir, shouldFail, name, dapprc ? "", testFlags ? "" }: pkgs.buildDappPackage { + inherit name shouldFail testFlags dapprc; solc=solc-0_6_7; src = dir; - dappFlags = "${dappFlags}"; deps = [ ds-test ds-token ds-math ]; checkInputs = with pkgs; [ hevm jq seth dapp solc ]; }; @@ -128,7 +127,36 @@ in dir = ./pass; name = "dappTestsShouldPass"; shouldFail = false; - dappFlags = "--max-iterations 50 --smttimeout 600000 --ffi"; + testFlags = "--max-iterations 50 --smttimeout 600000 --ffi -v"; + }; + + envVars = let + envVarTest = match : dapprc : runTest { + testFlags = "--match ${match}"; + dir = ./env; + name = "dappTestEnvVar"; + shouldFail = false; + inherit dapprc; + }; + seth = "${pkgs.seth}/bin/seth"; + in pkgs.recurseIntoAttrs { + # we get "hevm: insufficient balance for gas cost" if we run these together... + # maybe we need an env var to set the balance for the origin? + origin = envVarTest "origin.sol" "export DAPP_TEST_ORIGIN=$(${seth} --to-hex 256)"; + rest = envVarTest "rest.sol" '' + export DAPP_TEST_BALANCE=$(${seth} --to-wei 998877665544 ether) + export DAPP_TEST_ADDRESS=$(${seth} --to-hex 256) + export DAPP_TEST_CALLER=$(${seth} --to-hex 100) + export DAPP_TEST_GAS_CREATE=$(${seth} --to-wei 4.20 ether) + export DAPP_TEST_GAS_CALL=$(${seth} --to-wei 0.69 ether) + export DAPP_TEST_NONCE=100 + export DAPP_TEST_COINBASE=$(${seth} --to-hex 666) + export DAPP_TEST_NUMBER=420 + export DAPP_TEST_TIMESTAMP=69 + export DAPP_TEST_GAS_LIMIT=$(${seth} --to-wei 4206966 ether) + export DAPP_TEST_GAS_PRICE=100 + export DAPP_TEST_DIFFICULTY=600 + ''; }; shouldFail = let @@ -136,7 +164,7 @@ in dir = ./fail; shouldFail = true; name = "dappTestsShouldFail-${match}"; - dappFlags = "--match ${match} --smttimeout 600000"; + testFlags = "--match ${match} --smttimeout 600000"; }; in pkgs.recurseIntoAttrs { prove-add = fail "prove_add"; @@ -155,7 +183,7 @@ in solc = solc-0_6_7; src = dss-src; name = "dss"; - dappFlags = "--match '[^dai].t.sol'"; + testFlags = "--match '[^dai].t.sol'"; deps = [ ds-test ds-token ds-value ]; }; } diff --git a/src/dapp-tests/env/origin.sol b/src/dapp-tests/env/origin.sol new file mode 100644 index 000000000..5ca772f8c --- /dev/null +++ b/src/dapp-tests/env/origin.sol @@ -0,0 +1,8 @@ +import "ds-test/test.sol"; + +contract Env is DSTest { + // DAPP_TEST_ORIGIN + function testOrigin() public { + assertEq(tx.origin, address(256)); + } +} diff --git a/src/dapp-tests/env/rest.sol b/src/dapp-tests/env/rest.sol new file mode 100644 index 000000000..367075e2a --- /dev/null +++ b/src/dapp-tests/env/rest.sol @@ -0,0 +1,72 @@ +import "ds-test/test.sol"; + +contract Env is DSTest { + uint creationGas; + constructor() public { + creationGas = gasleft(); + } + + // TODO: why does this fail when address == 0? + // DAPP_TEST_BALANCE + function testBalance() public { + assertEq(address(this).balance, 998877665544 ether); + } + // DAPP_TEST_ADDRESS + function testAddress() public { + assertEq(address(this), address(256)); + } + // DAPP_TEST_NONCE + // we can't test the nonce directly, but can instead check the address of a newly deployed contract + function testNonce() public { + uint8 nonce = 100; + bytes memory payload = abi.encodePacked(hex"d694", address(this), nonce); + + address expected = address(uint160(uint256(keccak256(payload)))); + address actual = address(new Trivial()); + assertEq(actual, expected); + + } + // DAPP_TEST_CALLER + function testCaller() public { + assertEq(msg.sender, address(100)); + } + // DAPP_TEST_GAS_CREATE + function testGasCreate() public { + // we can't be exact since we had to spend some gas to write to storage... + assertLt(creationGas, 4.20 ether); + assertGt(creationGas, 4.1999999999999 ether); + } + // DAPP_TEST_GAS_CALL + function testGasCall() public { + uint gas = gasleft(); + // we can't be exact since we had to spend some gas to get here... + assertLt(gas, 0.69 ether); + assertGt(gas, 0.689999999999999 ether); + } + // DAPP_TEST_COINBASE + function testCoinbase() public { + assertEq(block.coinbase, address(666)); + } + // DAPP_TEST_NUMBER + function testBlockNumber() public { + assertEq(block.number, 420); + } + // DAPP_TEST_TIMESTAMP + function testTimestamp() public { + assertEq(block.timestamp, 69); + } + // DAPP_TEST_GAS_LIMIT + function testGasLimit() public { + assertEq(block.gaslimit, 4206966 ether); + } + // DAPP_TEST_GAS_PRICE + function testGasPrice() public { + assertEq(tx.gasprice, 100); + } + // DAPP_TEST_DIFFICULTY + function testDifficulty() public { + assertEq(block.difficulty, 600); + } +} + +contract Trivial {} diff --git a/src/dapp-tests/pass/cheatCodes.sol b/src/dapp-tests/pass/cheatCodes.sol index 1305284d2..40aa08ab3 100644 --- a/src/dapp-tests/pass/cheatCodes.sol +++ b/src/dapp-tests/pass/cheatCodes.sol @@ -11,14 +11,29 @@ interface Hevm { function sign(uint256,bytes32) external returns (uint8,bytes32,bytes32); function addr(uint256) external returns (address); function ffi(string[] calldata) external returns (bytes memory); + function file(bytes32,uint) external; + function file(bytes32,address) external; + function file(bytes32,address,uint) external; + function look(bytes32) external returns (uint); + function look(bytes32,address) external returns (uint); + function replace(address,bytes calldata) external; } contract HasStorage { uint slot0 = 10; } +contract Old { + uint constant public x = 10; +} + +contract New { + uint constant public x = 100; +} + contract CheatCodes is DSTest { address store = address(new HasStorage()); + Old target = new Old(); Hevm hevm = Hevm(HEVM_ADDRESS); function test_warp_concrete(uint128 jump) public { @@ -87,4 +102,54 @@ contract CheatCodes is DSTest { (string memory output) = abi.decode(hevm.ffi(inputs), (string)); assertEq(output, "acab"); } + + function testNonce(address who, uint n) public { + hevm.file("nonce", who, n); + assertEq(hevm.look("nonce", who), n); + } + + function testBalance(address who, uint bal) public { + hevm.file("balance", who, bal); + assertEq(who.balance, bal); + } + + function testReplace() public { + hevm.replace(address(target), type(New).runtimeCode); + assertEq(New(address(target)).x(), 100); + } + + function proveCaller(address who) public { + hevm.file("caller", who); + assertEq(msg.sender, who); + } + + function testOrigin(address who) public { + hevm.file("origin", who); + assertEq(tx.origin, who); + } + + function testCoinbase(address who) public { + hevm.file("coinbase", who); + assertEq(block.coinbase, who); + } + + function testGasPrice(uint price) public { + hevm.file("gasPrice", price); + assertEq(tx.gasprice, price); + } + + function testTxGasLimit(uint limit) public { + hevm.file("txGasLimit", limit); + assertEq(hevm.look("txGasLimit"), limit); + } + + function testBlockGasLimit(uint limit) public { + hevm.file("blockGasLimit", limit); + assertEq(block.gaslimit, limit); + } + + function testDifficulty(uint difficulty) public { + hevm.file(bytes32("difficulty"), difficulty); + assertEq(block.difficulty, difficulty); + } } diff --git a/src/hevm/CHANGELOG.md b/src/hevm/CHANGELOG.md index 6c4ad5f40..83f82fd7f 100644 --- a/src/hevm/CHANGELOG.md +++ b/src/hevm/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- A new configuration variable `DAPP_TEST_NONCE` has been added that allows control over the nonce of the testing contract + ### Changed - The configuration variable `DAPP_TEST_BALANCE_CREATE` has been renamed to `DAPP_TEST_BALANCE` diff --git a/src/hevm/README.md b/src/hevm/README.md index ddd02888a..a237256e2 100644 --- a/src/hevm/README.md +++ b/src/hevm/README.md @@ -239,6 +239,7 @@ These environment variables can be used to control block parameters: | `DAPP_TEST_GAS_CREATE` | `0xffffffffffff` | The gas to provide when creating the testing contract | | `DAPP_TEST_GAS_CALL` | `0xffffffffffff` | The gas to provide to each call made to the testing contract | | `DAPP_TEST_BALANCE` | `0xffffffffffffffffffffffff` | The balance to provide to `DAPP_TEST_ADDRESS` | +| `DAPP_TEST_NONCE ` | `1` | The initial nonce to use for `DAPP_TEST_ADDRESS` | | `DAPP_TEST_COINBASE` | `0x0000000000000000000000000000000000000000` | The coinbase address. Will be set to the coinbase for the block at `DAPP_TEST_NUMBER` if rpc is enabled | | `DAPP_TEST_NUMBER` | `0` | The block number. Will be set to the latest block if rpc is enabled | | `DAPP_TEST_TIMESTAMP` | `0` | The block timestamp. Will be set to the timestamp for the block at `DAPP_TEST_NUMBER` if rpc is enabled | diff --git a/src/hevm/src/EVM.hs b/src/hevm/src/EVM.hs index 33dd718e2..ec0968d53 100644 --- a/src/hevm/src/EVM.hs +++ b/src/hevm/src/EVM.hs @@ -52,7 +52,6 @@ import qualified Data.ByteArray as BA import qualified Data.Map.Strict as Map import qualified Data.Sequence as Seq import qualified Data.Tree.Zipper as Zipper -import qualified Data.Vector as V import qualified Data.Vector.Storable as Vector import qualified Data.Vector.Storable.Mutable as Vector @@ -2012,6 +2011,81 @@ cheatActions = assign (state . memory . word256At outOffset) res _ -> vmError (BadCheatCode sig), + action "file(bytes32,address,uint256)" $ + \sig _ _ input -> case decodeStaticArgs input of + [what', addr, val] -> forceConcrete what' $ \(C _ (utf8Word -> what)) -> + makeUnique addr $ \(C _ (num -> usr)) -> fetchAccount usr $ \_ -> + case what of + "nonce" -> forceConcrete val $ \v -> assign (env . contracts . ix usr . nonce) v + "balance" -> forceConcrete val $ \v -> assign (env . contracts . ix usr . balance) v + _ -> vmError (BadCheatCode sig) + _ -> vmError (BadCheatCode sig), + + action "file(bytes32,address)" $ + \sig _ _ input -> case decodeStaticArgs input of + [what', addr] -> forceConcrete what' $ \(C _ (utf8Word -> what)) -> + case what of + "caller" -> assign (state . caller) (SAddr . sFromIntegral . rawVal $ addr) + "origin" -> makeUnique addr $ \(C _ (num -> who)) -> assign (tx . origin) who + "coinbase" -> makeUnique addr $ \(C _ (num -> who)) -> assign (block . coinbase) who + _ -> vmError (BadCheatCode sig) + _ -> vmError (BadCheatCode sig), + + action "file(bytes32,uint256)" $ + \sig _ _ input -> case decodeStaticArgs input of + [what', val'] -> forceConcrete2 (what', val') $ \((C _ (utf8Word -> what)), val) -> + case what of + "gasPrice" -> assign (tx . gasprice) val + "txGasLimit" -> assign (tx . txgaslimit) val + "blockGasLimit" -> assign (block . gaslimit) val + "difficulty" -> assign (block . difficulty) val + _ -> vmError (BadCheatCode sig) + _ -> vmError (BadCheatCode sig), + + action "look(bytes32,address)" $ + \sig outOffset _ input -> + case decodeStaticArgs input of + [what', who'] -> forceConcrete what' $ \(C _ (utf8Word -> what)) -> + makeUnique who' $ \(C _ (num -> who))-> do + case what of + "nonce" -> do + vm <- get + case Map.lookup who $ view (env . contracts) vm of + Just c' -> do + let out = litWord (_nonce c') + assign (state . returndata . word256At 0) out + assign (state . memory . word256At outOffset) out + _ -> vmError (BadCheatCode sig) + _ -> vmError (BadCheatCode sig) + _ -> vmError (BadCheatCode sig), + + action "look(bytes32)" $ + \sig outOffset _ input -> case decodeStaticArgs input of + [what'] -> forceConcrete what' $ \(C _ (utf8Word -> what)) -> + case what of + "txGasLimit" -> do + vm <- get + let out = litWord $ view (tx . txgaslimit) vm + assign (state . returndata . word256At 0) out + assign (state . memory . word256At outOffset) out + _ -> vmError (BadCheatCode sig) + _ -> vmError (BadCheatCode sig), + + action "replace(address,bytes)" $ + \sig _ _ input -> case decodeBuffer [AbiAddressType, AbiBytesDynamicType] input of + CAbi vals -> case vals of + [AbiAddress usr, AbiBytesDynamic (ConcreteBuffer -> newCode)] -> do + vm <- get + if usr == (view (state . codeContract) vm) + then vmError (BadCheatCode sig) + else do + assign (env . contracts . (ix usr) . codeOps) $ mkCodeOps newCode + assign (env . contracts . (ix usr) . opIxMap) $ mkOpIxMap newCode + assign (env . contracts . (ix usr) . contractcode) $ RuntimeCode newCode + assign (cache . fetched . (ix usr) . contractcode) $ RuntimeCode newCode + _ -> vmError (BadCheatCode sig) + _ -> vmError (BadCheatCode sig), + action "sign(uint256,bytes32)" $ \sig outOffset _ input -> case decodeStaticArgs input of [sk, hash] -> @@ -2300,7 +2374,7 @@ finishFrame how = do modifying burned (subtract remainingGas) modifying (state . gas) (+ remainingGas) - FeeSchedule {..} = view ( block . schedule ) oldVm + FeeSchedule {} = view ( block . schedule ) oldVm -- Now dispatch on whether we were creating or calling, -- and whether we shall return, revert, or error (six cases). @@ -2552,15 +2626,14 @@ checkJump x xs = do self <- use (state . codeContract) theCodeOps <- use (env . contracts . ix self . codeOps) theOpIxMap <- use (env . contracts . ix self . opIxMap) - if x < num (len theCode) && 0x5b == (fromMaybe (error "tried to jump to symbolic code location") $ unliteral $ EVM.Symbolic.index (num x) theCode) - then - if OpJumpdest == snd (theCodeOps RegularVector.! (theOpIxMap Vector.! num x)) - then do - state . stack .= xs - state . pc .= num x - else - vmError BadJumpDestination - else vmError BadJumpDestination + if x < num (len theCode) + && 0x5b == (fromMaybe (error "tried to jump to symbolic code location") $ unliteral $ EVM.Symbolic.index (num x) theCode) + && OpJumpdest == snd (theCodeOps RegularVector.! (theOpIxMap Vector.! num x)) + then do + state . stack .= xs + state . pc .= num x + else do + vmError BadJumpDestination opSize :: Word8 -> Int opSize x | x >= 0x60 && x <= 0x7f = num x - 0x60 + 2 diff --git a/src/hevm/src/EVM/Dev.hs b/src/hevm/src/EVM/Dev.hs index 6524f51e5..c34abcb3f 100644 --- a/src/hevm/src/EVM/Dev.hs +++ b/src/hevm/src/EVM/Dev.hs @@ -47,8 +47,8 @@ loadDappInfo path file = _ -> error "nope, sorry" -ghciTest :: String -> String -> Maybe String -> IO [Bool] -ghciTest root path statePath = +ghciTest :: String -> String -> Maybe Text -> Maybe Int -> Maybe String -> IO [Bool] +ghciTest root path match verbosity statePath = withCurrentDirectory root $ do loadFacts <- case statePath of @@ -61,12 +61,12 @@ ghciTest root path statePath = let opts = UnitTestOptions { oracle = EVM.Fetch.zero - , verbose = Nothing + , verbose = verbosity , maxIter = Nothing , smtTimeout = Nothing , smtState = Nothing , solver = Nothing - , match = "" + , match = fromMaybe ".*" match , fuzzRuns = 100 , replay = Nothing , vmModifier = loadFacts @@ -78,7 +78,7 @@ ghciTest root path statePath = readSolc path >>= \case Just (contractMap, _) -> do - let unitTests = findAllUnitTests (Map.elems contractMap) + let unitTests = findUnitTests (EVM.UnitTest.match opts) $ Map.elems contractMap results <- runSMT $ query $ concatMapM (runUnitTestContract opts contractMap) unitTests let (passing, _) = unzip results pure passing diff --git a/src/hevm/src/EVM/Types.hs b/src/hevm/src/EVM/Types.hs index 8900fbcb1..9ce96ba46 100644 --- a/src/hevm/src/EVM/Types.hs +++ b/src/hevm/src/EVM/Types.hs @@ -556,3 +556,6 @@ abiKeccak = concatMapM :: Monad m => (a -> m [b]) -> [a] -> m [b] concatMapM f xs = liftM concat (mapM f xs) + +utf8Word :: W256 -> Text +utf8Word = Text.decodeUtf8 . BS.filter (/= 0) . word256Bytes diff --git a/src/hevm/src/EVM/UnitTest.hs b/src/hevm/src/EVM/UnitTest.hs index e7eae8e70..77582499c 100644 --- a/src/hevm/src/EVM/UnitTest.hs +++ b/src/hevm/src/EVM/UnitTest.hs @@ -87,20 +87,21 @@ data UnitTestOptions = UnitTestOptions } data TestVMParams = TestVMParams - { testAddress :: Addr - , testCaller :: Addr - , testOrigin :: Addr - , testGasCreate :: W256 - , testGasCall :: W256 - , testBalanceCreate :: W256 - , testCoinbase :: Addr - , testNumber :: W256 - , testTimestamp :: W256 - , testGaslimit :: W256 - , testGasprice :: W256 - , testMaxCodeSize :: W256 - , testDifficulty :: W256 - , testChainId :: W256 + { testAddress :: Addr + , testNonce :: W256 + , testCaller :: Addr + , testOrigin :: Addr + , testGasCreate :: W256 + , testGasCall :: W256 + , testBalance :: W256 + , testCoinbase :: Addr + , testNumber :: W256 + , testTimestamp :: W256 + , testGaslimit :: W256 + , testGasprice :: W256 + , testMaxCodeSize :: W256 + , testDifficulty :: W256 + , testChainId :: W256 } defaultGasForCreating :: W256 @@ -112,6 +113,9 @@ defaultGasForInvoking = 0xffffffffffff defaultBalanceForTestContract :: W256 defaultBalanceForTestContract = 0xffffffffffffffffffffffff +defaultNonceForTestContract :: W256 +defaultNonceForTestContract = 1 + defaultMaxCodeSize :: W256 defaultMaxCodeSize = 0xffffffff @@ -135,7 +139,10 @@ initializeUnitTest UnitTestOptions { .. } theContract = do Stepper.evm $ do -- Give a balance to the test target - env . contracts . ix addr . balance += w256 (testBalanceCreate testParams) + env . contracts . ix addr . balance += w256 (testBalance testParams) + + -- Set the test targets nonce + env . contracts . ix addr . nonce .= w256 (testNonce testParams) -- call setUp(), if it exists, to initialize the test contract let theAbi = view abiMap theContract @@ -927,8 +934,8 @@ initialUnitTestVm (UnitTestOptions {..}) theContract = } creator = initialContract (RuntimeCode mempty) - & set nonce 1 - & set balance (w256 testBalanceCreate) + & set nonce (w256 testNonce) + & set balance (w256 testBalance) in vm & set (env . contracts . at ethrunAddress) (Just creator) @@ -967,6 +974,7 @@ getParametersFromEnvironmentVariables rpc = do TestVMParams <$> getAddr "DAPP_TEST_ADDRESS" (createAddress ethrunAddress 1) + <*> getWord "DAPP_TEST_NONCE" defaultNonceForTestContract <*> getAddr "DAPP_TEST_CALLER" ethrunAddress <*> getAddr "DAPP_TEST_ORIGIN" ethrunAddress <*> getWord "DAPP_TEST_GAS_CREATE" defaultGasForCreating