diff --git a/packages/contracts-rfq/contracts/FastBridgeV2.sol b/packages/contracts-rfq/contracts/FastBridgeV2.sol index 67221da8d8..3402b2a4f3 100644 --- a/packages/contracts-rfq/contracts/FastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/FastBridgeV2.sol @@ -28,9 +28,8 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { mapping(bytes32 => BridgeTxDetails) public bridgeTxDetails; /// @notice Relay details on destination chain mapping(bytes32 => BridgeRelay) public bridgeRelayDetails; - - /// @dev to prevent replays - uint256 public nonce; + /// @notice Unique bridge nonces tracked per originSender + mapping(address => uint256) public senderNonces; // @dev the block the contract was deployed at uint256 public immutable deployBlock; @@ -113,6 +112,12 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { return _timeSince(bridgeTxDetails[transactionId].proofBlockTimestamp) > DISPUTE_PERIOD; } + /// @notice This function is deprecated and should not be used. + /// @dev Replaced by senderNonces + function nonce() external pure returns (uint256) { + return 0; + } + /// @inheritdoc IFastBridge function getBridgeTransaction(bytes memory request) external pure returns (BridgeTransaction memory) { // Note: when passing V2 request, this will decode the V1 fields correctly since the new fields were @@ -158,7 +163,7 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { originFeeAmount: originFeeAmount, sendChainGas: params.sendChainGas, deadline: params.deadline, - nonce: nonce++, // increment nonce on every bridge + nonce: senderNonces[params.sender]++, // increment nonce on every bridge exclusivityRelayer: paramsV2.quoteRelayer, // We checked exclusivityEndTime to be in range (0 .. params.deadline] above, so can safely cast exclusivityEndTime: uint256(exclusivityEndTime) diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol index 9a45037d54..3347f9e5fa 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol @@ -95,6 +95,11 @@ abstract contract FastBridgeV2SrcBaseTest is FastBridgeV2Test { fastBridge.refund(abi.encode(bridgeTx)); } + function test_nonce() public view { + // deprecated. should always return zero in FbV2. + assertEq(fastBridge.nonce(), 0); + } + function assertEq(FastBridgeV2.BridgeStatus a, FastBridgeV2.BridgeStatus b) public pure { assertEq(uint8(a), uint8(b)); } diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol index dd01387d72..c2a5902133 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol @@ -103,6 +103,8 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { expectBridgeRequested(tokenTx, txId); expectBridgeQuoteDetails(txId, tokenParamsV2.quoteId); bridge({caller: userA, msgValue: 0, params: tokenParams}); + assertEq(fastBridge.senderNonces(userA), 1); + assertEq(fastBridge.senderNonces(userB), 0); assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REQUESTED); checkTokenBalancesAfterBridge(userA); } @@ -112,6 +114,8 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { expectBridgeRequested(tokenTx, txId); expectBridgeQuoteDetails(txId, tokenParamsV2.quoteId); bridge({caller: userB, msgValue: 0, params: tokenParams}); + assertEq(fastBridge.senderNonces(userA), 1); + assertEq(fastBridge.senderNonces(userB), 0); assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REQUESTED); assertEq(srcToken.balanceOf(userA), LEFTOVER_BALANCE + tokenParams.originAmount); checkTokenBalancesAfterBridge(userB); @@ -126,10 +130,14 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { function test_bridge_eth() public { // bridge token first to match the nonce bridge({caller: userA, msgValue: 0, params: tokenParams}); + assertEq(fastBridge.senderNonces(userA), 1); + assertEq(fastBridge.senderNonces(userB), 0); bytes32 txId = getTxId(ethTx); expectBridgeRequested(ethTx, txId); expectBridgeQuoteDetails(txId, ethParamsV2.quoteId); bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + assertEq(fastBridge.senderNonces(userA), 2); + assertEq(fastBridge.senderNonces(userB), 0); assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REQUESTED); checkEthBalancesAfterBridge(userA); } @@ -137,18 +145,24 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { function test_bridge_eth_diffSender() public { // bridge token first to match the nonce bridge({caller: userA, msgValue: 0, params: tokenParams}); + assertEq(fastBridge.senderNonces(userA), 1); + assertEq(fastBridge.senderNonces(userB), 0); bytes32 txId = getTxId(ethTx); expectBridgeRequested(ethTx, txId); expectBridgeQuoteDetails(txId, ethParamsV2.quoteId); + // bridge for user A as sender, called by userB bridge({caller: userB, msgValue: ethParams.originAmount, params: ethParams}); + assertEq(fastBridge.senderNonces(userA), 2); + assertEq(fastBridge.senderNonces(userB), 0); assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REQUESTED); assertEq(userA.balance, LEFTOVER_BALANCE + ethParams.originAmount); checkEthBalancesAfterBridge(userB); } function test_bridge_userSpecificNonce() public { - vm.skip(true); // TODO: unskip when implemented bridge({caller: userA, msgValue: 0, params: tokenParams}); + assertEq(fastBridge.senderNonces(userA), 1); + assertEq(fastBridge.senderNonces(userB), 0); // UserB nonce is 0 ethTx.nonce = 0; ethParams.sender = userB; @@ -157,6 +171,8 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { expectBridgeRequested(ethTx, txId); expectBridgeQuoteDetails(txId, ethParamsV2.quoteId); bridge({caller: userB, msgValue: ethParams.originAmount, params: ethParams}); + assertEq(fastBridge.senderNonces(userA), 1); + assertEq(fastBridge.senderNonces(userB), 1); assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REQUESTED); checkEthBalancesAfterBridge(userB); }