Refactor balance override logic#4400
Conversation
…g strategies Moves balance override logic into dedicated `balance/` and `balance/aave` submodules, gives the `Detector` its own cache and web3 handle, and renames the main struct to `StateOverrides`. Deletes `configs::balance_overrides` (hardcoded per-token strategies and the `autodetect` toggle) — detection is now always automatic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request refactors the balance override system by moving logic into a dedicated balance module and consolidating detection and caching within the Detector struct. It also removes the ability to provide hardcoded token configurations via environment variables, favoring automatic detection. Two critical issues were identified in the new implementation: a buffer reuse bug in find_plausible_strategies_for_slots that corrupts Solady slot calculations, and an incorrect implementation of is_valid_for_all_holders which would cause holder-specific DirectSlot strategies to be incorrectly cached as holder-agnostic.
| buf[0..20].copy_from_slice(holder.as_slice()); | ||
| buf[28..32].copy_from_slice(BALANCE_SLOT_SEED); | ||
| let solady_slot = keccak256(&buf[0..32]); |
There was a problem hiding this comment.
The buf array is reused from the previous loop (lines 43-47) without being fully cleared. Specifically, buf[20..28] will contain stale data from the holder address copy at line 42 (buf[12..32]), which will result in an incorrect solady_slot hash. You should use a fresh 32-byte buffer for the Solady slot calculation to ensure correctness. Additionally, dereference the keccak256 result to use it as a [u8; 32] array.
| buf[0..20].copy_from_slice(holder.as_slice()); | |
| buf[28..32].copy_from_slice(BALANCE_SLOT_SEED); | |
| let solady_slot = keccak256(&buf[0..32]); | |
| let mut solady_buf = [0u8; 32]; | |
| solady_buf[0..20].copy_from_slice(holder.as_slice()); | |
| solady_buf[28..32].copy_from_slice(BALANCE_SLOT_SEED); | |
| let solady_slot = *keccak256(solady_buf); |
References
- The keccak256 function from alloy returns a B256 type. To use it as a [u8; 32] array, it must be dereferenced (e.g., *keccak256(data)).
| pub(crate) fn is_valid_for_all_holders(&self) -> bool { | ||
| matches!(self, Self::DirectSlot { .. } | Self::AaveV3AToken { .. }) | ||
| } |
There was a problem hiding this comment.
The logic for is_valid_for_all_holders is incorrect. DirectSlot contains a pre-computed storage slot that is typically specific to the holder used during detection (e.g., keccak256(holder, mapping_slot)), so it is NOT valid for all holders. Conversely, SolidityMapping, SoladyMapping, and AaveV3AToken are holder-agnostic strategies because they compute the holder-specific slot dynamically at override time. Caching a DirectSlot as holder-agnostic will lead to incorrect balance overrides for other users.
| pub(crate) fn is_valid_for_all_holders(&self) -> bool { | |
| matches!(self, Self::DirectSlot { .. } | Self::AaveV3AToken { .. }) | |
| } | |
| pub(crate) fn is_valid_for_all_holders(&self) -> bool { | |
| !matches!(self, Self::DirectSlot { .. }) | |
| } |
Description
In order to prepare the
balance-overridescrate to also handle approval overrides this PR moves the logic around slightly and splits off code that is useful for approval from code that is specific to balance computation.Changes
Additional changes
Web3instance into the aave specific strategy to simplify interface for computing the relevant storage slotsautodetectHow to test
existing tests are still passing
balance override logic is not broken
TODO
do another pass over this to make this nicer