Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
43 changes: 41 additions & 2 deletions base/src/libraries/TokenLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ library TokenLib {
/// @notice Thrown when the transfer amount is zero.
error ZeroAmount();

/// @notice Thrown when cumulative deposits exceed uint64 max when scaled to remote amount.
error CumulativeDepositExceedsU64();

//////////////////////////////////////////////////////////////
/// Events ///
//////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -137,8 +140,15 @@ library TokenLib {
localAmount = transfer.remoteAmount * scalar;
require(msg.value == localAmount, InvalidMsgValue());

_addToDeposits({
$: $,
localToken: transfer.localToken,
remoteToken: transfer.remoteToken,
scalar: scalar,
additionalLocalAmount: localAmount
});

tokenType = SolanaTokenType.WrappedToken;
$.deposits[transfer.localToken][transfer.remoteToken] += localAmount;
} else {
// Prevent sending ETH when bridging ERC20 tokens
require(msg.value == 0, InvalidMsgValue());
Expand Down Expand Up @@ -187,7 +197,13 @@ library TokenLib {
// IMPORTANT: Update the transfer struct IN MEMORY to reflect the remote amount to use for bridging.
transfer.remoteAmount = SafeCastLib.toUint64(receivedRemoteAmount);

$.deposits[transfer.localToken][transfer.remoteToken] += localAmount;
_addToDeposits({
$: $,
localToken: transfer.localToken,
remoteToken: transfer.remoteToken,
scalar: scalar,
additionalLocalAmount: localAmount
});

tokenType = SolanaTokenType.WrappedToken;
}
Expand Down Expand Up @@ -254,4 +270,27 @@ library TokenLib {
TokenLibStorage storage $ = getTokenLibStorage();
$.scalars[localToken][remoteToken] = 10 ** scalarExponent;
}

//////////////////////////////////////////////////////////////
/// Private Functions ///
//////////////////////////////////////////////////////////////

/// @notice Validates and updates cumulative deposits, ensuring they don't exceed uint64 max when scaled.
///
/// @param $ Storage reference to the TokenLibStorage struct.
/// @param localToken Address of the local token.
/// @param remoteToken Pubkey of the remote token.
/// @param scalar Conversion scalar for the token pair.
/// @param additionalLocalAmount Amount to add to deposits (in local units).
function _addToDeposits(
TokenLibStorage storage $,
address localToken,
Pubkey remoteToken,
uint256 scalar,
uint256 additionalLocalAmount
) private {
uint256 newDeposits = $.deposits[localToken][remoteToken] + additionalLocalAmount;
require(newDeposits / scalar <= type(uint64).max, CumulativeDepositExceedsU64());
$.deposits[localToken][remoteToken] = newDeposits;
}
}
53 changes: 53 additions & 0 deletions base/test/libraries/TokenLib.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,59 @@ contract TokenLibTest is CommonTest {
bridge.bridgeToken(transfer, emptyIxs);
}

function test_initializeTransfer_revertsOnCumulativeU64Overflow() public {
// Step 1: Create a local ERC20 token with 18 decimals
MockERC20 localToken = new MockERC20("Test Token", "TEST", 18);

// Give alice a large balance
localToken.mint(alice, 10000e18);

// Step 2: Register a remote token with a scalar exponent of 2
// This means: localAmount = remoteAmount * 10^2 (scalar = 100)
// Or: remoteAmount = localAmount / 100
Pubkey remoteToken = Pubkey.wrap(0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef);
uint8 scalarExponent = 2;

_registerTokenPair(address(localToken), remoteToken, scalarExponent, 0);

// Verify the scalar is correctly set
uint256 expectedScalar = 10 ** scalarExponent; // 100
assertEq(bridge.scalars(address(localToken), remoteToken), expectedScalar, "Scalar should be 100");

// Step 3: Calculate the remote amount for each bridge operation
// When we bridge 1000 tokens (1000 * 10^18 in smallest units):
// remoteAmount = localAmount / scalar = (1000 * 10^18) / 100 = 10^19
uint256 bridgeAmount = 1000e18;
uint256 expectedRemoteAmountPerBridge = bridgeAmount / expectedScalar;

// Step 4: Bridge 1000 units twice
Transfer memory transfer = Transfer({
localToken: address(localToken),
remoteToken: remoteToken,
to: bytes32(uint256(uint160(alice))),
remoteAmount: uint64(expectedRemoteAmountPerBridge) // This will be 10^19
});

Ix[] memory emptyIxs;

// First bridge - should succeed
vm.startPrank(alice);
localToken.approve(address(bridge), bridgeAmount);
bridge.bridgeToken(transfer, emptyIxs);
vm.stopPrank();

uint256 depositsAfterFirst = bridge.deposits(address(localToken), remoteToken);
assertEq(depositsAfterFirst, bridgeAmount, "Deposits after first bridge should equal bridge amount");

// Second bridge - should revert with CumulativeDepositExceedsU64
// because total would be 2 * 10^19 which exceeds uint64.max (18,446,744,073.709551615)
vm.startPrank(alice);
localToken.approve(address(bridge), bridgeAmount);
vm.expectRevert(TokenLib.CumulativeDepositExceedsU64.selector);
bridge.bridgeToken(transfer, emptyIxs);
vm.stopPrank();
}

//////////////////////////////////////////////////////////////
/// Finalize Transfer Tests ///
//////////////////////////////////////////////////////////////
Expand Down