-
Notifications
You must be signed in to change notification settings - Fork 449
feat: add depth-based worst-case attack benchmarks for execute mode #1885
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| /** | ||
| * @title AttackOrchestrator | ||
| * @dev Orchestrates attacks on CREATE2-deployed contracts by deriving their addresses | ||
| * and calling attack(uint256) on each one within a specified range. | ||
| */ | ||
| contract AttackOrchestrator { | ||
| // Immutable storage for gas efficiency | ||
| address public immutable deployerAddress; | ||
| bytes32 public immutable initCodeHash; | ||
|
|
||
| // Attack function selector for attack(uint256) | ||
| bytes4 private constant ATTACK_SELECTOR = 0x64dd891a; | ||
|
|
||
| /** | ||
| * @dev Constructor sets the deployer address and init code hash | ||
| * @param _deployer The address that deployed the target contracts via CREATE2 | ||
| * @param _initCodeHash The keccak256 hash of the target contract's init code | ||
| */ | ||
| constructor(address _deployer, bytes32 _initCodeHash) { | ||
| deployerAddress = _deployer; | ||
| initCodeHash = _initCodeHash; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Executes attacks on a range of CREATE2-deployed contracts | ||
| * @param value The value to pass to each attack() call | ||
| * @param startIndex The starting index (inclusive) | ||
| * @param endIndex The ending index (exclusive) | ||
| */ | ||
| function attack(uint256 value, uint256 startIndex, uint256 endIndex) external { | ||
| // Load immutables into local variables for assembly access | ||
| address _deployer = deployerAddress; | ||
| bytes32 _codeHash = initCodeHash; | ||
|
|
||
| // Use assembly for gas-efficient CREATE2 address derivation and calls | ||
| assembly { | ||
| // Use local variables instead of immutables | ||
| let deployer := _deployer | ||
| let codeHash := _codeHash | ||
|
|
||
| // Loop through the range | ||
| for { let i := startIndex } lt(i, endIndex) { i := add(i, 1) } { | ||
| // Derive CREATE2 address | ||
| // Formula: keccak256(0xff ++ deployer ++ salt ++ initCodeHash)[12:] | ||
|
|
||
| // Store CREATE2 preimage in memory | ||
| // The layout should be: 0xff (1 byte) + deployer (20 bytes) + salt (32 bytes) + codeHash (32 bytes) | ||
| // Total: 85 bytes | ||
| let memPtr := 0x00 | ||
| mstore8(memPtr, 0xff) // 0xff prefix at byte 0 | ||
| mstore(add(memPtr, 0x01), shl(96, deployer)) // deployer address at bytes 1-20 | ||
| mstore(add(memPtr, 0x15), i) // salt (i as uint256) at bytes 21-52 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For clarity I would not use hex here, but rather ints, so it is easier to check if these constants are correct. |
||
| mstore(add(memPtr, 0x35), codeHash) // init code hash at bytes 53-84 | ||
|
|
||
| // Compute CREATE2 address | ||
| let target := and( | ||
| keccak256(0x00, 0x55), // Hash 85 bytes | ||
| 0xffffffffffffffffffffffffffffffffffffffff // Mask to 20 bytes | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Masking is not necessary. If you |
||
| ) | ||
|
|
||
| // Prepare memory for the attack call at a different location to avoid overwriting | ||
| let callDataPtr := 0x80 | ||
| // Hardcode the selector directly: 0x64dd891a shifted left by 224 bits | ||
| mstore(callDataPtr, 0x64dd891a00000000000000000000000000000000000000000000000000000000) | ||
| mstore(add(callDataPtr, 0x04), value) // Value parameter | ||
|
|
||
| // Call attack(value) with 50,000 gas (increased for safety) | ||
| let success := call( | ||
| 50000, // gas | ||
| target, // to | ||
| 0, // value (0 ETH) | ||
| callDataPtr,// argsOffset (where we stored the call data) | ||
| 0x24, // argsSize (4 + 32 = 36 bytes) | ||
| 0, // retOffset (don't store return data) | ||
| 0 // retSize | ||
| ) | ||
|
|
||
| // Continue regardless of success | ||
| // No need to check success, just continue to next iteration | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @dev Fallback function to execute attacks when called directly | ||
| * This allows the contract to execute in its constructor if needed | ||
| */ | ||
| fallback() external { | ||
| // Could implement constructor-based attack here if needed | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| contract WorstCaseERC20 { | ||
| // ERC20 State | ||
| mapping(address => uint256) public balanceOf; | ||
| mapping(address => mapping(address => uint256)) public allowance; | ||
| uint256 public totalSupply; | ||
|
|
||
| // Token metadata - returning constants to save gas | ||
| string public constant name = "WorstCase"; | ||
| string public constant symbol = "WORST"; | ||
| uint8 public constant decimals = 18; | ||
|
|
||
| constructor() { | ||
| // Mint total supply to deployer | ||
| totalSupply = 1_000_000_000 * 10 ** 18; // 1 billion tokens | ||
| balanceOf[msg.sender] = totalSupply; | ||
|
|
||
| // Set all mined storage slots to 1 | ||
| assembly { | ||
| sstore(0xfeb1bc66963690bd7d902e86ccaf4e0fa1ea72277653d012a3fed288892770fc, 1) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the address miner also put as a constraint that the preimages have this "nibble by nibble" overlap for the required depth? Mostly asking because what matters for this to be deep branches is that the hashes of the storage slot numbers satisfy this, not the storage numbers themselves (i.e. the storage slots are hashed to define which is the storage trie address). (Maybe I'm missing something else from the whole PR setup!) Edit: or maybe the code that generates this contract is putting the hashes instead of the preimages? |
||
| sstore(0xf7df78bf2009da798ad808c14e99d4b0b1351493558cf3172d4c9ab38edcdad2, 1) | ||
| sstore(0xf71150ce002de523152da00b34a46728867ac39c68cfede6e5e6be804e36ad33, 1) | ||
| sstore(0xf71b286886de2caf0f4e66d925abafa1ecff0218dde77b91a0b103b3a969df72, 1) | ||
| sstore(0xf71b24d7942831944a4a6c9a9a650b26162af41802b57c6a349480b3139ec9c0, 1) | ||
| sstore(0xf71b2efa932b6b0e7fdaa9e725d8fff777744cbb015ade8e56b791ba7024e812, 1) | ||
| sstore(0xf71b2e5b7127047a0ea71e879c8a7fbf3aa423ab284ff119e2e7874bf34264a5, 1) | ||
| sstore(0xf71b2e583d6ed22307e56f66f0d15664401d31de25500eb08b2b6bc507a5f947, 1) | ||
| sstore(0xf71b2e583a4893d4003d080cb2c6524d4461283bd7a198ae5d17b877cb58188b, 1) | ||
| sstore(0xf71b2e583db6708d2eb5ed9f0aab56cbe346a1efcaf18c5eec5a177120a2d119, 1) | ||
| } | ||
| } | ||
|
|
||
| // Minimal ERC20 implementation | ||
| function transfer(address to, uint256 amount) public returns (bool) { | ||
| require(balanceOf[msg.sender] >= amount, "Insufficient balance"); | ||
| balanceOf[msg.sender] -= amount; | ||
| balanceOf[to] += amount; | ||
| return true; | ||
| } | ||
|
|
||
| function approve(address spender, uint256 amount) public returns (bool) { | ||
| allowance[msg.sender][spender] = amount; | ||
| return true; | ||
| } | ||
|
|
||
| function transferFrom( | ||
| address from, | ||
| address to, | ||
| uint256 amount | ||
| ) public returns (bool) { | ||
| require(balanceOf[from] >= amount, "Insufficient balance"); | ||
| require( | ||
| allowance[from][msg.sender] >= amount, | ||
| "Insufficient allowance" | ||
| ); | ||
|
|
||
| balanceOf[from] -= amount; | ||
| balanceOf[to] += amount; | ||
| allowance[from][msg.sender] -= amount; | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| // Attack method - writes to the deepest storage slot | ||
| function attack(uint256 value) external { | ||
| assembly { | ||
| sstore(0xf71b2e583db6708d2eb5ed9f0aab56cbe346a1efcaf18c5eec5a177120a2d119, value) | ||
| } | ||
| } | ||
|
|
||
| // Optional: getter to verify the deepest slot value | ||
| function getDeepest() external view returns (uint256 value) { | ||
| assembly { | ||
| value := sload(0xf71b2e583db6708d2eb5ed9f0aab56cbe346a1efcaf18c5eec5a177120a2d119) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| contract WorstCaseERC20 { | ||
| // ERC20 State | ||
| mapping(address => uint256) public balanceOf; | ||
| mapping(address => mapping(address => uint256)) public allowance; | ||
| uint256 public totalSupply; | ||
|
|
||
| // Token metadata - returning constants to save gas | ||
| string public constant name = "WorstCase"; | ||
| string public constant symbol = "WORST"; | ||
| uint8 public constant decimals = 18; | ||
|
|
||
| constructor() { | ||
| // Mint total supply to deployer | ||
| totalSupply = 1_000_000_000 * 10 ** 18; // 1 billion tokens | ||
| balanceOf[msg.sender] = totalSupply; | ||
|
|
||
| // Set all mined storage slots to 1 | ||
| assembly { | ||
| sstore(0x9c5d7d33cb1f559d7ed29650fa088a32637eef1d79581ac5127ccf7902e13eed, 1) | ||
| sstore(0x9706cd28b41f247fc747da6b2297133ce602144f381fa48159973783578591c2, 1) | ||
| sstore(0x976083ba3825f635d11b0c43555a7b6abc05267ae579036cb70426da10c5305c, 1) | ||
| sstore(0x976bb5123ad9865aba1a0268896c7852b8b30d5faa5c28fc5e9538032c93bdde, 1) | ||
| sstore(0x976bf64fc9f446948a7cc25ec22b8757d4f4cb7ec5bbe510d7b12b6519d6d235, 1) | ||
| sstore(0x976bf43129304651a9160157c2d96f889f1e1c2ba8532b09ffc146e1938bd841, 1) | ||
| sstore(0x976bf480157b06eacc1c7155aa75a879606c2aea028025e70a5a3a9451de5686, 1) | ||
| sstore(0x976bf4853471a9fe3068a6e3292eef29a436fd76e92468299a44845e192708ae, 1) | ||
| sstore(0x976bf48593c9d604f1aa324251e146cceef9fd69a4869dc256c8a71524d3635c, 1) | ||
| } | ||
| } | ||
|
|
||
| // Minimal ERC20 implementation | ||
| function transfer(address to, uint256 amount) public returns (bool) { | ||
| require(balanceOf[msg.sender] >= amount, "Insufficient balance"); | ||
| balanceOf[msg.sender] -= amount; | ||
| balanceOf[to] += amount; | ||
| return true; | ||
| } | ||
|
|
||
| function approve(address spender, uint256 amount) public returns (bool) { | ||
| allowance[msg.sender][spender] = amount; | ||
| return true; | ||
| } | ||
|
|
||
| function transferFrom( | ||
| address from, | ||
| address to, | ||
| uint256 amount | ||
| ) public returns (bool) { | ||
| require(balanceOf[from] >= amount, "Insufficient balance"); | ||
| require( | ||
| allowance[from][msg.sender] >= amount, | ||
| "Insufficient allowance" | ||
| ); | ||
|
|
||
| balanceOf[from] -= amount; | ||
| balanceOf[to] += amount; | ||
| allowance[from][msg.sender] -= amount; | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| // Attack method - writes to the deepest storage slot | ||
| function attack(uint256 value) external { | ||
| assembly { | ||
| sstore(0x976bf48593c9d604f1aa324251e146cceef9fd69a4869dc256c8a71524d3635c, value) | ||
| } | ||
| } | ||
|
|
||
| // Optional: getter to verify the deepest slot value | ||
| function getDeepest() external view returns (uint256 value) { | ||
| assembly { | ||
| value := sload(0x976bf48593c9d604f1aa324251e146cceef9fd69a4869dc256c8a71524d3635c) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are constants stored in this for loop which do not need to be there. You can move these constant mstores before the loop (to init the constant parts of memory once)