Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion bindings/bin/l2tokenregistry_deployed.hex

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion bindings/bindings/l2tokenregistry.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion bindings/bindings/l2tokenregistry_more.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion contracts/contracts/l2/system/IL2TokenRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface IL2TokenRegistry {
/// @notice Token information structure
struct TokenInfo {
address tokenAddress; // ERC20 token contract address
bytes32 balanceSlot; // Token balance storage slot, bytes32(0) -> nil
bytes32 balanceSlot; // Token balance storage slot (Note: stored as actualSlot+1 internally, but returned as actual value in queries)
bool isActive; // Whether the token is active
uint8 decimals; // Token decimals
uint256 scale; // Core convention: rateScaled = tokenScale * (tokenPrice / ethPrice) * 10^(ethDecimals - tokenDecimals)
Expand Down
43 changes: 39 additions & 4 deletions contracts/contracts/l2/system/L2TokenRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,32 @@ contract L2TokenRegistry is IL2TokenRegistry, OwnableUpgradeable, ReentrancyGuar
supportedTokenSet.remove(uint256(_tokenID));
}

/**
* @notice Internal function: Convert actual balanceSlot to stored value (adds 1)
* @param _actualSlot The actual balance slot value
* @return The stored balance slot value (actualSlot + 1)
*/
function _toStoredBalanceSlot(bytes32 _actualSlot) internal pure returns (bytes32) {
bytes32 storedSlot;
assembly {
storedSlot := add(_actualSlot, 1)
}
return storedSlot;
}

/**
* @notice Internal function: Convert stored balanceSlot to actual value (subtracts 1)
* @param _storedSlot The stored balance slot value
* @return The actual balance slot value (storedSlot - 1)
*/
function _toActualBalanceSlot(bytes32 _storedSlot) internal pure returns (bytes32) {
bytes32 actualSlot;
assembly {
actualSlot := sub(_storedSlot, 1)
}
return actualSlot;
}

/**
* @notice Internal function: Register a single token
*/
Expand All @@ -182,10 +208,12 @@ contract L2TokenRegistry is IL2TokenRegistry, OwnableUpgradeable, ReentrancyGuar
} catch {
// If call fails, use default value 18
}

// Register token (isActive defaults to false)
// Note: balanceSlot is stored as actualSlot + 1
tokenRegistry[_tokenID] = TokenInfo({
tokenAddress: _tokenAddress,
balanceSlot: _balanceSlot,
balanceSlot: _toStoredBalanceSlot(_balanceSlot),
isActive: false,
decimals: decimals,
scale: _scale
Expand Down Expand Up @@ -227,11 +255,13 @@ contract L2TokenRegistry is IL2TokenRegistry, OwnableUpgradeable, ReentrancyGuar
} catch {
// If call fails, use default value 18
}

// Update registration information
// Note: balanceSlot is stored as actualSlot + 1
address oldAddress = tokenRegistry[_tokenID].tokenAddress;
tokenRegistry[_tokenID] = TokenInfo({
tokenAddress: _tokenAddress,
balanceSlot: _balanceSlot,
balanceSlot: _toStoredBalanceSlot(_balanceSlot),
isActive: _isActive,
decimals: decimals,
scale: _scale
Expand Down Expand Up @@ -383,11 +413,16 @@ contract L2TokenRegistry is IL2TokenRegistry, OwnableUpgradeable, ReentrancyGuar
/**
* @notice Get token information
* @param _tokenID Token ID
* @return TokenInfo structure
* @return TokenInfo structure with actual balanceSlot (automatically -1 from stored value)
*/
function getTokenInfo(uint16 _tokenID) external view returns (TokenInfo memory) {
if (tokenRegistry[_tokenID].tokenAddress == address(0)) revert TokenNotFound();
return tokenRegistry[_tokenID];

TokenInfo memory info = tokenRegistry[_tokenID];
// Convert stored balanceSlot to actual value
info.balanceSlot = _toActualBalanceSlot(info.balanceSlot);

return info;
}

/**
Expand Down
100 changes: 100 additions & 0 deletions contracts/contracts/test/L2TokenRegistry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,106 @@ contract L2TokenRegistryTest is Test {
priceOracle.getTokenIdByAddress(address(usdc));
}

/*//////////////////////////////////////////////////////////////
BalanceSlot Storage Tests
//////////////////////////////////////////////////////////////*/

function test_balanceSlot_storage_query_with_minus_one() public {
// Register token with balanceSlot = 9
vm.prank(owner);
priceOracle.registerToken(TOKEN_ID_USDC, address(usdc), BALANCE_SLOT_USDC, SCALE_USDC);

// Get balanceSlot through getTokenInfo (should return actual value = 9)
L2TokenRegistry.TokenInfo memory info = priceOracle.getTokenInfo(TOKEN_ID_USDC);
assertEq(info.balanceSlot, BALANCE_SLOT_USDC);

// Read balanceSlot directly from storage
// tokenRegistry is at slot 151
// TokenInfo struct layout:
// - slot 0: tokenAddress (20 bytes)
// - slot 1: balanceSlot (32 bytes)
// - slot 2: isActive (1 byte) + decimals (1 byte) + scale (32 bytes packed)
uint256 mappingSlot = 151;

// Calculate storage location: keccak256(tokenID || mappingSlot)
bytes32 key = keccak256(abi.encode(TOKEN_ID_USDC, mappingSlot));

// balanceSlot is stored in key + 1
bytes32 balanceSlotStorageLocation = bytes32(uint256(key) + 1);

// Read stored value from storage
bytes32 storedBalanceSlot = vm.load(address(priceOracle), balanceSlotStorageLocation);

// Stored value should be actualSlot + 1 = 9 + 1 = 10
assertEq(uint256(storedBalanceSlot), uint256(BALANCE_SLOT_USDC) + 1);

// Apply -1 to get actual value
bytes32 actualBalanceSlot = bytes32(uint256(storedBalanceSlot) - 1);

// Verify that manual -1 gives us the same value as getTokenInfo
assertEq(actualBalanceSlot, BALANCE_SLOT_USDC);
assertEq(actualBalanceSlot, info.balanceSlot);
}

function test_balanceSlot_storage_query_with_slot_zero() public {
// Test with balanceSlot = 0 (edge case)
bytes32 balanceSlot0 = bytes32(uint256(0));

vm.prank(owner);
priceOracle.registerToken(TOKEN_ID_USDT, address(usdt), balanceSlot0, SCALE_USDT);

// Get balanceSlot through getTokenInfo (should return actual value = 0)
L2TokenRegistry.TokenInfo memory info = priceOracle.getTokenInfo(TOKEN_ID_USDT);
assertEq(info.balanceSlot, balanceSlot0);

// Read balanceSlot directly from storage
uint256 mappingSlot = 151;
bytes32 key = keccak256(abi.encode(TOKEN_ID_USDT, mappingSlot));
bytes32 balanceSlotStorageLocation = bytes32(uint256(key) + 1);
bytes32 storedBalanceSlot = vm.load(address(priceOracle), balanceSlotStorageLocation);

// Stored value should be actualSlot + 1 = 0 + 1 = 1
assertEq(uint256(storedBalanceSlot), 1);

// Apply -1 to get actual value
bytes32 actualBalanceSlot = bytes32(uint256(storedBalanceSlot) - 1);

// Verify that manual -1 gives us 0
assertEq(actualBalanceSlot, balanceSlot0);
assertEq(uint256(actualBalanceSlot), 0);
assertEq(actualBalanceSlot, info.balanceSlot);
}

function test_balanceSlot_storage_query_multiple_tokens() public {
// Register multiple tokens with different balanceSlots
vm.prank(owner);
priceOracle.registerToken(TOKEN_ID_USDC, address(usdc), bytes32(uint256(9)), SCALE_USDC);
vm.prank(owner);
priceOracle.registerToken(TOKEN_ID_USDT, address(usdt), bytes32(uint256(10)), SCALE_USDT);
vm.prank(owner);
priceOracle.registerToken(TOKEN_ID_DAI, address(dai), bytes32(uint256(11)), SCALE_DAI);

uint256 mappingSlot = 151;

// Verify USDC: stored=10, actual=9
bytes32 key = keccak256(abi.encode(TOKEN_ID_USDC, mappingSlot));
bytes32 storedValue = vm.load(address(priceOracle), bytes32(uint256(key) + 1));
assertEq(uint256(storedValue), 10);
assertEq(bytes32(uint256(storedValue) - 1), priceOracle.getTokenInfo(TOKEN_ID_USDC).balanceSlot);

// Verify USDT: stored=11, actual=10
key = keccak256(abi.encode(TOKEN_ID_USDT, mappingSlot));
storedValue = vm.load(address(priceOracle), bytes32(uint256(key) + 1));
assertEq(uint256(storedValue), 11);
assertEq(bytes32(uint256(storedValue) - 1), priceOracle.getTokenInfo(TOKEN_ID_USDT).balanceSlot);

// Verify DAI: stored=12, actual=11
key = keccak256(abi.encode(TOKEN_ID_DAI, mappingSlot));
storedValue = vm.load(address(priceOracle), bytes32(uint256(key) + 1));
assertEq(uint256(storedValue), 12);
assertEq(bytes32(uint256(storedValue) - 1), priceOracle.getTokenInfo(TOKEN_ID_DAI).balanceSlot);
}

/*//////////////////////////////////////////////////////////////
Token Update Tests
//////////////////////////////////////////////////////////////*/
Expand Down
Loading