From 6b79fbcf6f3f6a84819e36a1851378c39da72176 Mon Sep 17 00:00:00 2001 From: yonkoo11 Date: Sat, 11 Jul 2026 22:18:18 +0100 Subject: [PATCH] fix(StdCheats): enable packed slots when dealing token balances Tokens that pack balance into a shared storage word (USDC-class, Optimism sUSD) fail stdstore find without packed mode. Use enable_packed_slots() for ERC20/1155/721 balance writes so deal updates only the balance bits and preserves neighboring data. Fixes #293 --- src/StdCheats.sol | 12 ++++++------ test/StdCheats.t.sol | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/StdCheats.sol b/src/StdCheats.sol index 95a5bf95..bc816849 100644 --- a/src/StdCheats.sol +++ b/src/StdCheats.sol @@ -827,8 +827,8 @@ abstract contract StdCheats is StdCheatsSafe { (, bytes memory balData) = token.staticcall(abi.encodeWithSelector(0x70a08231, to)); uint256 prevBal = abi.decode(balData, (uint256)); - // update balance - _stdstore.target(token).sig(0x70a08231).with_key(to).checked_write(give); + // update balance (packed-aware for tokens like USDC / sUSD that pack balance into a slot) + _stdstore.enable_packed_slots().target(token).sig(0x70a08231).with_key(to).checked_write(give); // update total supply if (adjust) { @@ -850,7 +850,7 @@ abstract contract StdCheats is StdCheatsSafe { uint256 prevBal = abi.decode(balData, (uint256)); // update balance - _stdstore.target(token).sig(0x00fdd58e).with_key(to).with_key(id).checked_write(give); + _stdstore.enable_packed_slots().target(token).sig(0x00fdd58e).with_key(to).with_key(id).checked_write(give); // update total supply if (adjust) { @@ -884,9 +884,9 @@ abstract contract StdCheats is StdCheatsSafe { (, bytes memory toBalData) = token.staticcall(abi.encodeWithSelector(0x70a08231, to)); uint256 toPrevBal = abi.decode(toBalData, (uint256)); - // update balances - _stdstore.target(token).sig(0x70a08231).with_key(abi.decode(ownerData, (address))).checked_write(--fromPrevBal); - _stdstore.target(token).sig(0x70a08231).with_key(to).checked_write(++toPrevBal); + // update balances (packed-aware) + _stdstore.enable_packed_slots().target(token).sig(0x70a08231).with_key(abi.decode(ownerData, (address))).checked_write(--fromPrevBal); + _stdstore.enable_packed_slots().target(token).sig(0x70a08231).with_key(to).checked_write(++toPrevBal); // update owner _stdstore.target(token).sig(0x6352211e).with_key(id).checked_write(to); diff --git a/test/StdCheats.t.sol b/test/StdCheats.t.sol index 868829dd..eebd0e45 100644 --- a/test/StdCheats.t.sol +++ b/test/StdCheats.t.sol @@ -122,6 +122,21 @@ contract StdCheatsTest is Test { assertEq(barToken.totalSupply(), 10000e18); } + function test_DealTokenPackedBalance() public { + // Balance packed in the low 128 bits of a storage word (USDC / sUSD class). + PackedBalanceToken token = new PackedBalanceToken(); + address other = address(0xBEEF); + token.seed(address(this), 100, 0xdead); + token.seed(other, 50, 0xbeef); + + deal(address(token), address(this), 999); + assertEq(token.balanceOf(address(this)), 999); + // high bits must be preserved + assertEq(token.other(address(this)), 0xdead); + assertEq(token.balanceOf(other), 50); + assertEq(token.other(other), 0xbeef); + } + function test_DealERC1155Token() public { BarERC1155 barToken = new BarERC1155(); address bar = address(barToken); @@ -672,6 +687,25 @@ contract BarERC721 { mapping(address => uint256) private _balances; } + +/// ERC20-like balance packed with adjacent metadata in the same slot (low 128 = balance). +contract PackedBalanceToken { + // layout per address: [other:128][balance:128] + mapping(address => uint256) private _slot; + + function seed(address account, uint128 bal, uint128 meta) external { + _slot[account] = uint256(bal) | (uint256(meta) << 128); + } + + function balanceOf(address account) external view returns (uint256) { + return uint128(_slot[account]); + } + + function other(address account) external view returns (uint256) { + return uint256(uint128(_slot[account] >> 128)); + } +} + contract RevertingContract { constructor() { revert();