diff --git a/docs/src/reference/modules/accounts.md b/docs/src/reference/modules/accounts.md index e08dddee..6c53cecc 100644 --- a/docs/src/reference/modules/accounts.md +++ b/docs/src/reference/modules/accounts.md @@ -48,13 +48,26 @@ Creates an address using the hash of the specified `name` as the private key and Creates an address using the hash of the specified `name` as the private key and adds a label to the address. +#### **`getDeploymentAddress(address who, uint64 nonce) → (address)`** + +Calculates the deployment address of `who` with nonce `nonce`. + +#### **`getDeploymentAddress(address who) → (address)`** + +Calculates the deployment address of `who` with the current nonce. + #### **`setStorage(address self, bytes32 slot, bytes32 value) → (address)`** Sets the specified `slot` in the storage of the given `self` address to the provided `value`. #### **`setNonce(address self, uint64 n) → (address)`** -Sets the nonce of the given `self` address to the provided value `n`. +Sets the nonce of the given `self` address to the provided value `n`. It will revert if the new +nonce is lower than the current address nonce. + +#### **`setNonceUnsafe(address self, uint64 n) → (address)`** + +Sets the nonce of the given `self` address to the provided arbitrary value `n`. #### **`impersonateOnce(address self) → (address)`** diff --git a/docs/src/reference/modules/context.md b/docs/src/reference/modules/context.md index 68809685..e25f1ffa 100644 --- a/docs/src/reference/modules/context.md +++ b/docs/src/reference/modules/context.md @@ -57,6 +57,14 @@ sets the `block.basefee` to `baseFee` sets the `block.basefee` to `baseFee` +#### **`setBlockPrevrandao(Context self, bytes32 newPrevrandao) → (Context)`** + +sets the `block.prevrandao` to `newPrevrandao` + +#### **`setBlockPrevrandao(bytes32 newPrevrandao) → (Context)`** + +sets the `block.prevrandao` to `newPrevrandao` + #### **`setChainId(Context self, uint64 chainId) → (Context)`** sets the `block.chainid` to `chainId` @@ -73,6 +81,14 @@ Sets the block coinbase to `who`. Sets the block coinbase to `who`. +#### **`setGasPrice(Context self, address newGasPrice) → (Context)`** + +Sets the gas price to `newGasPrice`. + +#### **`setGasPrice(address newGasPrice) → (Context)`** + +Sets the gas price to `newGasPrice`. + #### **`expectRevert(bytes revertData)`** Function used to check whether the next call reverts or not. @@ -113,6 +129,22 @@ Used to check if a call to `callee` with `data` was made. Used to check if a call to `callee` with `data` and `msgValue` was made. +#### **`expectCallMinGas(address callee, uint256 msgValue, uint64 minGas, bytes calldata data)`** + +Expect a call from `callee` with the specified `msgValue` and `data`, and a minimum amount of gas `minGas`. + +#### **`expectCallMinGas(address callee, uint256 msgValue, uint64 minGas, bytes calldata data, uint64 count)`** + +Expect a number of calls `count` from `callee` with the specified `msgValue` and `data`, and a minimum amount of gas `minGas`. + +#### **`expectSafeMemory(uint64 min, uint64 max)`** + +Allows to write on memory only between [0x00, 0x60) and [`min`, `max`) in the current subcontext + +#### **`expectsafememorycall(uint64 min, uint64 max)`** + +Allows to write on memory only between [0x00, 0x60) and [`min`, `max`) in the next subcontext + #### **`snapshot(Context) → (uint256)`** Takes a snapshot of the current state of the vm and returns an identifier. @@ -129,3 +161,19 @@ Reverts the state of the vm to the snapshot with id `snapshotId`. Reverts the state of the vm to the snapshot with id `snapshotId`. +#### **`addBreakpoint(Context self, string memory name)`** + +Creates a breakpoint to jump to in the debugger with `name`. + +#### **`addBreakpoint(string memory name)`** + +Creates a breakpoint to jump to in the debugger with `name`. + +#### **`addConditionalBreakpoint(Context self, string memory name, bool condition)`** + +Creates a conditional breakpoint to jump to in the debugger with name `name` and condition `condition`. + +#### **`addConditionalBreakpoint(string memory name, bool condition)`** + +Creates a conditional breakpoint to jump to in the debugger with name `name` and condition `condition`. + diff --git a/lib/forge-std b/lib/forge-std index 2b58ecbc..74cfb77e 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 2b58ecbcf3dfde7a75959dc7b4eb3d0670278de6 +Subproject commit 74cfb77e308dd188d2f58864aaf44963ae6b88b1 diff --git a/src/_modules/Accounts.sol b/src/_modules/Accounts.sol index 1951a2a5..bece367a 100644 --- a/src/_modules/Accounts.sol +++ b/src/_modules/Accounts.sol @@ -102,6 +102,43 @@ library accountsSafe { return label(addr, lbl); } + + /// @dev Calculates the deployment address of `who` with nonce `nonce`. + /// @param who The deployer address. + /// @param nonce The deployer nonce. + function getDeploymentAddress(address who, uint64 nonce) internal pure returns (address) { + bytes memory data; + + if (nonce == 0x00) { + data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), who, bytes1(0x80)); + } else if (nonce <= 0x7f) { + data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), who, uint8(nonce)); + } else if (nonce <= 0xff) { + data = abi.encodePacked(bytes1(0xd7), bytes1(0x94), who, bytes1(0x81), uint8(nonce)); + } else if (nonce <= 0xffff) { + data = abi.encodePacked(bytes1(0xd8), bytes1(0x94), who, bytes1(0x82), uint16(nonce)); + } else if (nonce <= 0xffffff) { + data = abi.encodePacked(bytes1(0xd9), bytes1(0x94), who, bytes1(0x83), uint24(nonce)); + } else if (nonce <= 0xffffffff) { + data = abi.encodePacked(bytes1(0xda), bytes1(0x94), who, bytes1(0x84), uint32(nonce)); + } else if (nonce <= 0xffffffffff) { + data = abi.encodePacked(bytes1(0xdb), bytes1(0x94), who, bytes1(0x85), uint40(nonce)); + } else if (nonce <= 0xffffffffffff) { + data = abi.encodePacked(bytes1(0xdc), bytes1(0x94), who, bytes1(0x86), uint48(nonce)); + } else if (nonce <= 0xffffffffffffff) { + data = abi.encodePacked(bytes1(0xdd), bytes1(0x94), who, bytes1(0x87), uint56(nonce)); + } else if (nonce <= 0xffffffffffffffff) { + data = abi.encodePacked(bytes1(0xde), bytes1(0x94), who, bytes1(0x88), uint64(nonce)); + } + + return address(uint160(uint256(keccak256(data)))); + } + + /// @dev Calculates the deployment address of `who` with the current nonce. + /// @param who The deployer address. + function getDeploymentAddress(address who) internal view returns (address) { + return getDeploymentAddress(who, getNonce(who)); + } } library accounts { @@ -167,6 +204,19 @@ library accounts { return accountsSafe.create(name, lbl); } + /// @dev Calculates the deployment address of `who` with nonce `nonce`. + /// @param who The deployer address. + /// @param nonce The deployer nonce. + function getDeploymentAddress(address who, uint64 nonce) internal pure returns (address) { + return accountsSafe.getDeploymentAddress(who, nonce); + } + + /// @dev Calculates the deployment address of `who` with the current nonce. + /// @param who The deployer address. + function getDeploymentAddress(address who) internal view returns (address) { + return accountsSafe.getDeploymentAddress(who); + } + /// @dev Sets the specified `slot` in the storage of the given `self` address to the provided `value`. /// @param self The address to modify the storage of. /// @param slot The storage slot to set. @@ -177,7 +227,8 @@ library accounts { return self; } - /// @dev Sets the nonce of the given `self` address to the provided value `n`. + /// @dev Sets the nonce of the given `self` address to the provided value `n`. It will revert if + // the new nonce is lower than the current address nonce. /// @param self The address to set the nonce for. /// @param n The value to set the nonce to. /// @return The updated address with the modified nonce. @@ -186,6 +237,15 @@ library accounts { return self; } + /// @dev Sets the nonce of the given `self` address to the arbitrary provided value `n`. + /// @param self The address to set the nonce for. + /// @param n The value to set the nonce to. + /// @return The updated address with the modified nonce. + function setNonceUnsafe(address self, uint64 n) internal returns (address) { + vulcan.hevm.setNonceUnsafe(self, n); + return self; + } + /// @dev Sets the `msg.sender` of the next call to `self`. /// @param self The address to impersonate. /// @return The address that was impersonated. diff --git a/src/_modules/Context.sol b/src/_modules/Context.sol index cda6349f..d7f2fde8 100644 --- a/src/_modules/Context.sol +++ b/src/_modules/Context.sol @@ -3,6 +3,8 @@ pragma solidity >=0.8.13 <0.9.0; import "./Vulcan.sol"; import "./Accounts.sol"; +import "./Strings.sol"; +import "../_utils/println.sol"; type Context is bytes32; @@ -69,6 +71,30 @@ library ctxSafe { function resumeGasMetering() internal { vulcan.hevm.resumeGasMetering(); } + + function startGasReport(string memory name) internal { + if (bytes(name).length > 32) { + revert("ctx.startGasReport: Gas report name can't have more than 32 characters"); + } + + bytes32 b32Name = bytes32(bytes(name)); + bytes32 slot = keccak256(bytes("vulcan.ctx.gasReport.name")); + accounts.setStorage(address(vulcan.hevm), slot, b32Name); + bytes32 valueSlot = keccak256(abi.encodePacked("vulcan.ctx.gasReport", b32Name)); + accounts.setStorage(address(vulcan.hevm), valueSlot, bytes32(gasleft())); + } + + function endGasReport() internal view { + uint256 gas = gasleft(); + bytes32 slot = keccak256(bytes("vulcan.ctx.gasReport.name")); + bytes32 b32Name = accounts.readStorage(address(vulcan.hevm), slot); + bytes32 valueSlot = keccak256(abi.encodePacked("vulcan.ctx.gasReport", b32Name)); + uint256 prevGas = uint256(accounts.readStorage(address(vulcan.hevm), valueSlot)); + if (gas > prevGas) { + revert("ctx.endGasReport: Gas used can't have a negative value"); + } + println(string.concat("gas(", string(abi.encodePacked(b32Name)), "):", strings.toString(prevGas - gas))); + } } library ctx { @@ -120,6 +146,14 @@ library ctx { ctxSafe.resumeGasMetering(); } + function startGasReport(string memory name) internal { + ctxSafe.startGasReport(name); + } + + function endGasReport() internal view { + ctxSafe.endGasReport(); + } + /// @dev Checks whether the current call is a static call or not. /// @return True if the current call is a static call, false otherwise. function isStaticcall() internal view returns (bool) { @@ -165,6 +199,19 @@ library ctx { return setBlockBaseFee(Context.wrap(0), baseFee); } + /// @dev Sets block.prevrandao. + /// @param newPrevrandao The new `block.prevrandao`. + function setBlockPrevrandao(Context self, bytes32 newPrevrandao) internal returns (Context) { + vulcan.hevm.prevrandao(newPrevrandao); + return self; + } + + /// @dev Sets block.prevrandao. + /// @param newPrevrandao The new `block.prevrandao`. + function setBlockPrevrandao(bytes32 newPrevrandao) internal returns (Context) { + return setBlockPrevrandao(Context.wrap(0), newPrevrandao); + } + /// @dev sets the `block.chainid` to `chainId` /// @param chainId the new block chain id function setChainId(Context self, uint64 chainId) internal returns (Context) { @@ -194,6 +241,19 @@ library ctx { return setBlockCoinbase(Context.wrap(0), who); } + /// @dev Sets the transaction gas price. + /// @param newGasPrice The new transaction gas price. + function setGasPrice(Context self, uint256 newGasPrice) internal returns (Context) { + vulcan.hevm.txGasPrice(newGasPrice); + return self; + } + + /// @dev Sets the transaction gas price. + /// @param newGasPrice The new transaction gas price. + function setGasPrice(uint256 newGasPrice) internal returns (Context) { + return setGasPrice(Context.wrap(0), newGasPrice); + } + /// @dev Function used to check whether the next call reverts or not. /// @param revertData The function call data that that is expected to fail. function expectRevert(bytes memory revertData) internal { @@ -269,6 +329,43 @@ library ctx { vulcan.hevm.expectCall(callee, msgValue, data); } + /// @dev Expect a call to an address with the specified msg.value and calldata, and a minimum amount of gas. + /// @param callee The address that is expected to be called. + /// @param msgValue The `msg.value` that is expected to be sent. + /// @param minGas The expected minimum amount of gas for the call. + /// @param data The call data that is expected to be used. + function expectCallMinGas(address callee, uint256 msgValue, uint64 minGas, bytes calldata data) internal { + vulcan.hevm.expectCallMinGas(callee, msgValue, minGas, data); + } + + /// @dev Expect a number call to an address with the specified msg.value and calldata, and a minimum amount of gas. + /// @param callee The address that is expected to be called. + /// @param msgValue The `msg.value` that is expected to be sent. + /// @param minGas The expected minimum amount of gas for the call. + /// @param data The call data that is expected to be used. + /// @param count The number of calls that are expected. + function expectCallMinGas(address callee, uint256 msgValue, uint64 minGas, bytes calldata data, uint64 count) + external + { + vulcan.hevm.expectCallMinGas(callee, msgValue, minGas, data, count); + } + + /// @dev Allows to write on memory only between [0x00, 0x60) and [min, max) in the current. + /// subcontext. + /// @param min The lower limit of the allowed memory slot. + /// @param max The upper limit of the allowed memory slot. + function expectSafeMemory(uint64 min, uint64 max) external { + vulcan.hevm.expectSafeMemory(min, max); + } + + /// @dev Allows to write on memory only between [0x00, 0x60) and [min, max) in the next + // subcontext. + /// @param min The lower limit of the allowed memory slot. + /// @param max The upper limit of the allowed memory slot. + function expectsafememorycall(uint64 min, uint64 max) external { + vulcan.hevm.expectSafeMemoryCall(min, max); + } + /// @dev Takes a snapshot of the current state of the vm and returns an identifier. /// @return The snapshot identifier. function snapshot(Context) internal returns (uint256) { @@ -294,6 +391,34 @@ library ctx { function revertToSnapshot(uint256 snapshotId) internal returns (bool) { return revertToSnapshot(Context.wrap(0), snapshotId); } + + /// @dev Creates a breakpoint to jump to in the debugger. + /// @param name The name of the breakpoint. + function addBreakpoint(Context self, string memory name) internal returns (Context) { + vulcan.hevm.breakpoint(name); + return self; + } + + /// @dev Creates a breakpoint to jump to in the debugger. + /// @param name The name of the breakpoint. + function addBreakpoint(string memory name) internal returns (Context) { + return addBreakpoint(Context.wrap(0), name); + } + + /// @dev Creates a breakpoint to jump to in the debugger. + /// @param name The name of the breakpoint. + /// @param condition The condition that needs to be fulfilled in order to add the breakpoint. + function addConditionalBreakpoint(Context self, string memory name, bool condition) internal returns (Context) { + vulcan.hevm.breakpoint(name, condition); + return self; + } + + /// @dev Creates a breakpoint to jump to in the debugger. + /// @param name The name of the breakpoint. + /// @param condition The condition that needs to be fulfilled in order to add the breakpoint. + function addConditionalBreakpoint(string memory name, bool condition) internal returns (Context) { + return addConditionalBreakpoint(Context.wrap(0), name, condition); + } } using ctx for Context global; diff --git a/src/_modules/Fs.sol b/src/_modules/Fs.sol index 8a7e0f60..88486b53 100644 --- a/src/_modules/Fs.sol +++ b/src/_modules/Fs.sol @@ -38,7 +38,7 @@ library fs { /// @dev Obtains the metadata of the specified file or directory. /// @param fileOrDir The path to the file or directory. /// @return data The metadata of the file or directory. - function metadata(string memory fileOrDir) internal returns (FsMetadata memory data) { + function metadata(string memory fileOrDir) internal view returns (FsMetadata memory data) { Hevm.FsMetadata memory md = vulcan.hevm.fsMetadata(fileOrDir); assembly { data := md @@ -103,7 +103,7 @@ library fs { /// @dev Checks if a file or directory exists. /// @param path The file or directory to check. /// @return Whether the file on `path` exists or not. - function fileExists(string memory path) internal returns (bool) { + function fileExists(string memory path) internal view returns (bool) { try vulcan.hevm.fsMetadata(path) { return true; } catch Error(string memory) { diff --git a/src/_modules/Gas.sol b/src/_modules/Gas.sol new file mode 100644 index 00000000..23abfd62 --- /dev/null +++ b/src/_modules/Gas.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "./Vulcan.sol"; +import "./Accounts.sol"; + +library gas { + bytes32 constant GAS_MEASUREMENTS_MAGIC = keccak256("vulcan.gas.measurements.magic"); + + function record(string memory name) internal { + bytes32 startSlot = keccak256(abi.encode(GAS_MEASUREMENTS_MAGIC, name, "start")); + accounts.setStorage(address(vulcan.hevm), startSlot, bytes32(gasleft())); + } + + function stopRecord(string memory name) internal returns (uint256) { + uint256 endGas = gasleft(); + + bytes32 startSlot = keccak256(abi.encode(GAS_MEASUREMENTS_MAGIC, name, "start")); + uint256 startGas = uint256(accounts.readStorage(address(vulcan.hevm), startSlot)); + + if (endGas > startGas) { + revert("gas.stopRecord: Gas used can't have a negative value"); + } + + bytes32 endSlot = keccak256(abi.encode(GAS_MEASUREMENTS_MAGIC, name, "end")); + accounts.setStorage(address(vulcan.hevm), endSlot, bytes32(endGas)); + + return startGas - endGas; + } + + function getRecord(string memory name) internal view returns (uint256, uint256) { + bytes32 startSlot = keccak256(abi.encode(GAS_MEASUREMENTS_MAGIC, name, "start")); + uint256 startGas = uint256(accounts.readStorage(address(vulcan.hevm), startSlot)); + + bytes32 endSlot = keccak256(abi.encode(GAS_MEASUREMENTS_MAGIC, name, "end")); + uint256 endGas = uint256(accounts.readStorage(address(vulcan.hevm), endSlot)); + + return (startGas, endGas); + } + + function used(string memory name) internal view returns (uint256) { + (uint256 startGas, uint256 endGas) = getRecord(name); + + return startGas - endGas; + } +} diff --git a/src/_utils/println.sol b/src/_utils/println.sol index 85995568..5ee09668 100644 --- a/src/_utils/println.sol +++ b/src/_utils/println.sol @@ -4,10 +4,10 @@ pragma solidity >=0.8.13 <0.9.0; import {console} from "../_modules/Console.sol"; import {fmt} from "../_modules/Fmt.sol"; -function println(string memory template, bytes memory args) view { +function println(string memory template, bytes memory args) pure { console.log(fmt.format(template, args)); } -function println(string memory arg) view { +function println(string memory arg) pure { console.log(arg); } diff --git a/src/test.sol b/src/test.sol index 7cb9a5fc..2ba35e69 100644 --- a/src/test.sol +++ b/src/test.sol @@ -12,6 +12,7 @@ import {events} from "./_modules/Events.sol"; import {expect} from "./_modules/Expect.sol"; import {forks, Fork} from "./_modules/Forks.sol"; import {fs, FsMetadata} from "./_modules/Fs.sol"; +import {gas} from "./_modules/Gas.sol"; import {huff, Huffc} from "./_modules/Huff.sol"; import {json, JsonObject} from "./_modules/Json.sol"; import {strings} from "./_modules/Strings.sol"; diff --git a/test/ExampleTest.sol b/test/ExampleTest.sol index 33b18c48..6ade669a 100644 --- a/test/ExampleTest.sol +++ b/test/ExampleTest.sol @@ -21,7 +21,7 @@ contract ExampleTest is Test { expect(false).toEqual(false); } - function testConsoleLog() external view { + function testConsoleLog() external pure { console.log("hello world"); } diff --git a/test/_modules/Accounts.t.sol b/test/_modules/Accounts.t.sol index 93f19311..90eaa4d3 100644 --- a/test/_modules/Accounts.t.sol +++ b/test/_modules/Accounts.t.sol @@ -1,6 +1,6 @@ pragma solidity >=0.8.13 <0.9.0; -import {Test, expect, commands, accounts, console} from "../../src/test.sol"; +import {Test, expect, commands, accounts, console, ctx} from "../../src/test.sol"; import {Sender} from "../mocks/Sender.sol"; contract AccountsTest is Test { @@ -202,6 +202,32 @@ contract AccountsTest is Test { expect(token.balanceOf(user)).toEqual(balance - firstBurn - secondBurn); expect(token.totalSupply()).toEqual(totalSupply - firstBurn - secondBurn); } + + function testGetDeploymentAddressWithCurrentNonce(address user, uint64 nonce) external { + ctx.assume(nonce < type(uint64).max); + + address deploymentAddress = user.setNonce(uint64(nonce)).getDeploymentAddress(); + + user.impersonateOnce(); + + address deployedAddress = address(new TestToken()); + + expect(deployedAddress).toEqual(deploymentAddress); + } + + function testGetDeploymentAddress(address user, uint64 nonce) external { + ctx.assume(nonce < type(uint64).max); + + address deploymentAddress = user.getDeploymentAddress(nonce); + + user.setNonce(nonce); + + user.impersonateOnce(); + + address deployedAddress = address(new TestToken()); + + expect(deployedAddress).toEqual(deploymentAddress); + } } contract TestToken { diff --git a/test/_modules/Context.t.sol b/test/_modules/Context.t.sol index 372cf35b..d15d6963 100644 --- a/test/_modules/Context.t.sol +++ b/test/_modules/Context.t.sol @@ -94,6 +94,14 @@ contract ContextTest is Test { target.value{value: uint256(1337)}(); } + + function testItCanReportGas() external { + ctx.startGasReport("test"); + for (uint256 i = 0; i < 5; i++) { + new MockTarget(); + } + ctx.endGasReport(); + } } contract MockTarget { diff --git a/test/_modules/Gas.t.sol b/test/_modules/Gas.t.sol new file mode 100644 index 00000000..c2738355 --- /dev/null +++ b/test/_modules/Gas.t.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {Test, expect, gas} from "../../src/test.sol"; + +contract GasTest is Test { + function testItMeasures() public { + string memory name = "test"; + + gas.record(name); + keccak256(bytes(name)); + uint256 measurementValue = gas.stopRecord(name); + + expect(measurementValue).toBeGreaterThan(0); + expect(measurementValue).toEqual(gas.used(name)); + } +}