diff --git a/base/src/libraries/TokenLib.sol b/base/src/libraries/TokenLib.sol index e27e547..a0294a5 100644 --- a/base/src/libraries/TokenLib.sol +++ b/base/src/libraries/TokenLib.sol @@ -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 /// ////////////////////////////////////////////////////////////// @@ -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()); @@ -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; } @@ -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; + } } diff --git a/base/test/libraries/TokenLib.t.sol b/base/test/libraries/TokenLib.t.sol index 895b888..b7e48a9 100644 --- a/base/test/libraries/TokenLib.t.sol +++ b/base/test/libraries/TokenLib.t.sol @@ -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 /// //////////////////////////////////////////////////////////////