Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Copy link
Copy Markdown
Member

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)

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Masking is not necessary. If you CALL something then the EVM will mask it (so hide the topmost 12 bytes)

)

// 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
}
}
79 changes: 79 additions & 0 deletions tests/benchmark/stateful/bloatnet/depth_benchmarks/depth_10.sol
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)
Copy link
Copy Markdown
Collaborator

@jsign jsign Dec 16, 2025

Choose a reason for hiding this comment

The 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)
}
}
}
78 changes: 78 additions & 0 deletions tests/benchmark/stateful/bloatnet/depth_benchmarks/depth_9.sol
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)
}
}
}
Loading
Loading