-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Dynamically detect token storage slots when necessary
Certain protocols include token contracts in their swap logic. Previously, this could cause balance mocks to fail by attempting to initialize an account that was already initialized with mock contract code, resulting in a silent no-op. With this update, such cases are detected by inspecting the `involved_contracts` attribute. If a token's address is found in the involved contracts, the PoolState class will now dynamically identify the correct storage slots for balances and addresses. These slots will then be used for mocking balances in future operations, ensuring proper handling of such scenarios.
- Loading branch information
kayibal
committed
Oct 17, 2024
1 parent
232b966
commit d2dbc26
Showing
7 changed files
with
287 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
from .adapter_contract import ProtoSimContract | ||
from .utils import ERC20OverwriteFactory | ||
from .constants import EXTERNAL_ACCOUNT | ||
from . import SimulationEngine | ||
from ..models import EVMBlock, EthereumToken | ||
|
||
_MARKER_VALUE = 314159265358979323846264338327950288419716939937510 | ||
_SPENDER = "0x08d967bb0134F2d07f7cfb6E246680c53927DD30" | ||
|
||
class SlotDetectionFailure(Exception): | ||
pass | ||
|
||
def brute_force_slots( | ||
t: EthereumToken, block: EVMBlock, engine: SimulationEngine | ||
) -> tuple[int, int]: | ||
"""Brute-force detection of storage slots for token allowances and balances. | ||
This function attempts to determine the storage slots used by the token contract for | ||
balance and allowance values by systematically testing different storage locations. | ||
It uses EVM simulation to overwrite storage slots (from 0 to 19) and checks whether | ||
the overwritten slot produces the expected result by making VM calls to | ||
`balanceOf(...)` or `allowance(...)`. | ||
The token contract and its storage must already be set up within the engine's | ||
database before calling this function. | ||
Parameters | ||
---------- | ||
t : EthereumToken | ||
The token whose storage slots are being brute-forced. | ||
block : EVMBlock | ||
The block at which the simulation is executed. | ||
engine : SimulationEngine | ||
The engine used to simulate the blockchain environment. | ||
Returns | ||
------- | ||
tuple[int, int] | ||
A tuple containing the detected balance storage slot and the allowance | ||
storage slot, respectively. | ||
Raises | ||
------ | ||
SlotDetectionFailure | ||
If the function fails to detect a valid slot for either balances or allowances | ||
after checking all possible slots (0-19). | ||
""" | ||
token_contract = ProtoSimContract(t.address, "ERC20", engine) | ||
balance_slot = None | ||
for i in range(20): | ||
overwrite_factory = ERC20OverwriteFactory(t, (i, 1)) | ||
overwrite_factory.set_balance(_MARKER_VALUE, EXTERNAL_ACCOUNT) | ||
res = token_contract.call( | ||
"balanceOf", | ||
[EXTERNAL_ACCOUNT], | ||
block_number=block.id, | ||
timestamp=int(block.ts.timestamp()), | ||
overrides=overwrite_factory.get_protosim_overwrites(), | ||
caller=EXTERNAL_ACCOUNT, | ||
value=0, | ||
) | ||
if res.return_value is None: | ||
continue | ||
if res.return_value[0] == _MARKER_VALUE: | ||
balance_slot = i | ||
break | ||
|
||
allowance_slot = None | ||
for i in range(20): | ||
overwrite_factory = ERC20OverwriteFactory(t, (0, i)) | ||
overwrite_factory.set_allowance(_MARKER_VALUE, _SPENDER, EXTERNAL_ACCOUNT) | ||
res = token_contract.call( | ||
"allowance", | ||
[EXTERNAL_ACCOUNT, _SPENDER], | ||
block_number=block.id, | ||
timestamp=int(block.ts.timestamp()), | ||
overrides=overwrite_factory.get_protosim_overwrites(), | ||
caller=EXTERNAL_ACCOUNT, | ||
value=0, | ||
) | ||
if res.return_value is None: | ||
continue | ||
if res.return_value[0] == _MARKER_VALUE: | ||
allowance_slot = i | ||
break | ||
|
||
if balance_slot is None: | ||
raise SlotDetectionFailure(f"Failed to infer balance slot for {t.address}") | ||
|
||
if allowance_slot is None: | ||
raise SlotDetectionFailure(f"Failed to infer allowance slot for {t.address}") | ||
|
||
return balance_slot, allowance_slot | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
from protosim_py.evm import AccountInfo, StateUpdate, BlockHeader, SimulationEngine | ||
from protosim_py.evm.constants import MAX_BALANCE | ||
from protosim_py.evm.utils import exec_rpc_method, get_code_for_address | ||
from protosim_py.models import Address, EVMBlock | ||
|
||
|
||
def read_account_storage_from_rpc( | ||
address: Address, block_hash: str, connection_string: str = None | ||
) -> dict[str, str]: | ||
"""Reads complete storage of a contract from a Geth instance. | ||
Parameters | ||
---------- | ||
address: | ||
The contracts address | ||
block_hash: | ||
The block hash at which we want to retrieve storage at. | ||
connection_string: | ||
The connection string for the Geth rpc endpoint. | ||
Returns | ||
------- | ||
storage: | ||
A dictionary containing the hex encoded slots (both keys and values). | ||
""" | ||
|
||
res = exec_rpc_method( | ||
connection_string, | ||
"debug_storageRangeAt", | ||
[block_hash, 0, address, "0x00", 0x7FFFFFFF], | ||
) | ||
|
||
storage = {} | ||
for i in res["storage"].values(): | ||
try: | ||
if i["key"] is None: | ||
raise RuntimeError( | ||
"Node with preimages required, found a slot without key!" | ||
) | ||
k = i["key"] | ||
if i["value"] is None: | ||
continue | ||
else: | ||
v = i["value"] | ||
storage[k] = v | ||
except (TypeError, ValueError): | ||
raise RuntimeError( | ||
"Encountered invalid storage data retrieved data from geth -> " + str(i) | ||
) | ||
return storage | ||
|
||
|
||
def init_contract_via_rpc( | ||
block: EVMBlock, | ||
contract_address: Address, | ||
engine: SimulationEngine, | ||
connection_string: str, | ||
): | ||
"""Initializes a contract in the simulation engine using data fetched via RPC. | ||
This function retrieves the contract's bytecode and storage from an external RPC | ||
endpoint and uses it to initialize the contract within the simulation engine. | ||
Additionally, it sets up necessary default accounts and updates the contract's | ||
state based on the provided block. | ||
Parameters | ||
---------- | ||
block : | ||
The block at which to initialize the contract. | ||
contract_address : | ||
The address of the contract to be initialized. | ||
engine : | ||
The simulation engine instance where the contract is set up. | ||
connection_string : | ||
RPC connection string used to fetch contract data. | ||
Returns | ||
------- | ||
SimulationEngine | ||
The simulation engine with the contract initialized. | ||
""" | ||
bytecode = get_code_for_address(contract_address, connection_string) | ||
storage = read_account_storage_from_rpc( | ||
contract_address, block.hash_, connection_string | ||
) | ||
engine.init_account( | ||
address="0x0000000000000000000000000000000000000000", | ||
account=AccountInfo(balance=0, nonce=0), | ||
mocked=False, | ||
permanent_storage=None, | ||
) | ||
engine.init_account( | ||
address="0x0000000000000000000000000000000000000004", | ||
account=AccountInfo(balance=0, nonce=0), | ||
mocked=False, | ||
permanent_storage=None, | ||
) | ||
engine.init_account( | ||
address=contract_address, | ||
account=AccountInfo( | ||
balance=MAX_BALANCE, | ||
nonce=0, | ||
code=bytecode, | ||
), | ||
mocked=False, | ||
permanent_storage=None, | ||
) | ||
engine.update_state( | ||
{ | ||
contract_address: StateUpdate( | ||
storage={ | ||
int.from_bytes( | ||
bytes.fromhex(k[2:]), "big", signed=False | ||
): int.from_bytes(bytes.fromhex(v[2:]), "big", signed=False) | ||
for k, v in storage.items() | ||
}, | ||
balance=0, | ||
) | ||
}, | ||
BlockHeader( | ||
number=block.id, hash=block.hash_, timestamp=int(block.ts.timestamp()) | ||
), | ||
) | ||
return engine |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import os | ||
|
||
import pytest | ||
|
||
from protosim_py.evm.storage import TychoDBSingleton | ||
from protosim_py.evm.token import brute_force_slots | ||
from protosim_py.evm.utils import ( | ||
create_engine, | ||
) | ||
from test.evm.utils import init_contract_via_rpc | ||
from protosim_py.models import EthereumToken, EVMBlock | ||
|
||
_ETH_RPC_URL = os.getenv("ETH_RPC_URL") | ||
|
||
|
||
@pytest.mark.skipif( | ||
_ETH_RPC_URL is None, | ||
reason="Geth RPC access required. Please via `ETH_RPC_URL` env variable.", | ||
) | ||
def test_brute_force_slots(): | ||
block = EVMBlock( | ||
20984206, "0x01a709ad31a9ff223f7932ae8f6d6762e02b114250393adf128a2858b39c4b9d" | ||
) | ||
token_address = "0xac3E018457B222d93114458476f3E3416Abbe38F" | ||
token = EthereumToken("sFRAX", token_address, 18) | ||
TychoDBSingleton.initialize() | ||
engine = create_engine([], trace=True) | ||
engine = init_contract_via_rpc(block, token_address, engine, _ETH_RPC_URL) | ||
|
||
balance_slots, allowance_slot = brute_force_slots(token, block, engine) | ||
|
||
assert balance_slots == 3 | ||
assert allowance_slot == 4 |