27
27
REFERENCE_SPEC_GIT_PATH = "TODO"
28
28
REFERENCE_SPEC_VERSION = "TODO"
29
29
30
- MAX_CONTRACT_SIZE = 24 * 1024 # TODO: This could be a fork property
31
-
32
30
XOR_TABLE_SIZE = 256
33
31
XOR_TABLE = [Hash (i ).sha256 () for i in range (XOR_TABLE_SIZE )]
34
32
@@ -65,10 +63,45 @@ def test_worst_bytecode_single_opcode(
65
63
The test is performed in the last block of the test, and the entire block gas limit is
66
64
consumed by repeated opcode executions.
67
65
"""
68
- # We use 100G gas limit to be able to deploy a large number of contracts in a single block,
69
- # avoiding bloating the number of preparing blocks in the test.
70
- env = Environment (gas_limit = 100_000_000_000 )
71
- attack_gas_limit = Environment ().gas_limit
66
+ # The attack gas limit is the gas limit which the target tx will use
67
+ # The test will scale the block gas limit to setup the contracts accordingly to be
68
+ # able to pay for the contract deposit. This has to take into account the 200 gas per byte,
69
+ # but also the quadratic memory expansion costs which have to be paid each time the
70
+ # memory is being setup
71
+ attack_gas_limit = 100_000_000
72
+ max_contract_size = fork .max_code_size () or 24 * 1024 # Uses Spurious Dragon limit by default
73
+
74
+ gas_costs = fork .gas_costs ()
75
+
76
+ # Calculate the absolute minimum gas costs to deploy the contract
77
+ # This does not take into account setting up the actual memory (using KECCAK256 and XOR)
78
+ # so the actual costs of deploying the contract is higher
79
+ memory_expansion_gas_calculator = fork .memory_expansion_gas_calculator ()
80
+ memory_gas_minimum = memory_expansion_gas_calculator (new_bytes = len (bytes (max_contract_size )))
81
+ code_deposit_gas_minimum = (
82
+ fork .gas_costs ().G_CODE_DEPOSIT_BYTE * max_contract_size + memory_gas_minimum
83
+ )
84
+
85
+ intrinsic_gas_cost_calc = fork .transaction_intrinsic_cost_calculator ()
86
+ # Calculate the loop cost of the attacker to query one address
87
+ loop_cost = (
88
+ gas_costs .G_KECCAK_256 # KECCAK static cost
89
+ + math .ceil (85 / 32 ) * gas_costs .G_KECCAK_256_WORD # KECCAK dynamic cost for CREATE2
90
+ + gas_costs .G_VERY_LOW * 3 # ~MSTOREs+ADDs
91
+ + gas_costs .G_COLD_ACCOUNT_ACCESS # Opcode cost
92
+ + 30 # ~Gluing opcodes
93
+ )
94
+ # Calculate the number of contracts to be targeted
95
+ num_contracts = (
96
+ # Base available gas = GAS_LIMIT - intrinsic - (out of loop MSTOREs)
97
+ attack_gas_limit - intrinsic_gas_cost_calc () - gas_costs .G_VERY_LOW * 4
98
+ ) // loop_cost
99
+
100
+ # Set the block gas limit to a relative high value to ensure the code deposit tx
101
+ # fits in the block (there is enough gas available in the block to execute this)
102
+ # TODO: verify this, I think we can just do `code_deposit_gas_minimum * 2 * num_contracts`?
103
+ # and then take the max of this and the attack gas limit
104
+ env = Environment (gas_limit = attack_gas_limit * 2 * (code_deposit_gas_minimum // loop_cost + 1 ))
72
105
73
106
# The initcode will take its address as a starting point to the input to the keccak
74
107
# hash function.
@@ -86,13 +119,13 @@ def test_worst_bytecode_single_opcode(
86
119
)
87
120
+ Op .POP
88
121
),
89
- condition = Op .LT (Op .MSIZE , MAX_CONTRACT_SIZE ),
122
+ condition = Op .LT (Op .MSIZE , max_contract_size ),
90
123
)
91
124
# Despite the whole contract has random bytecode, we make the first opcode be a STOP
92
125
# so CALL-like attacks return as soon as possible, while EXTCODE(HASH|SIZE) work as
93
126
# intended.
94
127
+ Op .MSTORE8 (0 , 0x00 )
95
- + Op .RETURN (0 , MAX_CONTRACT_SIZE )
128
+ + Op .RETURN (0 , max_contract_size )
96
129
)
97
130
initcode_address = pre .deploy_contract (code = initcode )
98
131
@@ -127,24 +160,10 @@ def test_worst_bytecode_single_opcode(
127
160
)
128
161
factory_caller_address = pre .deploy_contract (code = factory_caller_code )
129
162
130
- gas_costs = fork .gas_costs ()
131
- intrinsic_gas_cost_calc = fork .transaction_intrinsic_cost_calculator ()
132
- loop_cost = (
133
- gas_costs .G_KECCAK_256 # KECCAK static cost
134
- + math .ceil (85 / 32 ) * gas_costs .G_KECCAK_256_WORD # KECCAK dynamic cost for CREATE2
135
- + gas_costs .G_VERY_LOW * 3 # ~MSTOREs+ADDs
136
- + gas_costs .G_COLD_ACCOUNT_ACCESS # Opcode cost
137
- + 30 # ~Gluing opcodes
138
- )
139
- num_contracts = (
140
- # Base available gas = GAS_LIMIT - intrinsic - (out of loop MSTOREs)
141
- attack_gas_limit - intrinsic_gas_cost_calc () - gas_costs .G_VERY_LOW * 4
142
- ) // loop_cost
143
-
144
163
contracts_deployment_tx = Transaction (
145
164
to = factory_caller_address ,
146
165
gas_limit = env .gas_limit ,
147
- gas_price = 10 ** 9 ,
166
+ gas_price = 10 ** 6 ,
148
167
data = Hash (num_contracts ),
149
168
sender = pre .fund_eoa (),
150
169
)
@@ -180,11 +199,11 @@ def test_worst_bytecode_single_opcode(
180
199
)
181
200
)
182
201
183
- if len (attack_code ) > MAX_CONTRACT_SIZE :
202
+ if len (attack_code ) > max_contract_size :
184
203
# TODO: A workaround could be to split the opcode code into multiple contracts
185
204
# and call them in sequence.
186
205
raise ValueError (
187
- f"Code size { len (attack_code )} exceeds maximum code size { MAX_CONTRACT_SIZE } "
206
+ f"Code size { len (attack_code )} exceeds maximum code size { max_contract_size } "
188
207
)
189
208
opcode_address = pre .deploy_contract (code = attack_code )
190
209
opcode_tx = Transaction (
0 commit comments