diff --git a/src/Facets/RelayFacet.sol b/src/Facets/RelayFacet.sol index e3abbba5c..1b23ee53b 100644 --- a/src/Facets/RelayFacet.sol +++ b/src/Facets/RelayFacet.sol @@ -21,6 +21,10 @@ contract RelayFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { // Relayer wallet for ERC20 transfers address public immutable relaySolver; + /// Storage /// + + mapping(bytes32 => bool) public consumedIds; + /// Types /// /// @dev Relay specific parameters @@ -55,6 +59,11 @@ contract RelayFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { ILiFi.BridgeData memory _bridgeData, RelayData calldata _relayData ) { + // Ensure that the id isn't already consumed + if (consumedIds[_relayData.requestId]) { + revert InvalidQuote(); + } + // Verify that the bridging quote has been signed by the Relay solver // as attested using the attestation API // API URL: https://api.relay.link/requests/{requestId}/signature/v2 @@ -182,6 +191,8 @@ contract RelayFacet is ILiFi, ReentrancyGuard, SwapperV2, Validatable { } } + consumedIds[_relayData.requestId] = true; + // Emit special event if bridging to non-EVM chain if (_bridgeData.receiver == LibAsset.NON_EVM_ADDRESS) { emit BridgeToNonEVMChain( diff --git a/test/solidity/Facets/RelayFacet.t.sol b/test/solidity/Facets/RelayFacet.t.sol index e58ee3068..cfd81689b 100644 --- a/test/solidity/Facets/RelayFacet.t.sol +++ b/test/solidity/Facets/RelayFacet.t.sol @@ -31,6 +31,10 @@ contract TestRelayFacet is RelayFacet { ) external pure returns (uint256) { return _getMappedChainId(chainId); } + + function setConsumedId(bytes32 id) external { + consumedIds[id] = true; + } } contract RelayFacetTest is TestBaseFacet { @@ -47,7 +51,7 @@ contract RelayFacetTest is TestBaseFacet { customBlockNumberForForking = 19767662; initTestBase(); relayFacet = new TestRelayFacet(RELAY_RECEIVER, RELAY_SOLVER); - bytes4[] memory functionSelectors = new bytes4[](5); + bytes4[] memory functionSelectors = new bytes4[](6); functionSelectors[0] = relayFacet.startBridgeTokensViaRelay.selector; functionSelectors[1] = relayFacet .swapAndStartBridgeTokensViaRelay @@ -57,6 +61,7 @@ contract RelayFacetTest is TestBaseFacet { .setFunctionApprovalBySignature .selector; functionSelectors[4] = relayFacet.getMappedChainId.selector; + functionSelectors[5] = relayFacet.setConsumedId.selector; addFacet(diamond, address(relayFacet), functionSelectors); relayFacet = TestRelayFacet(address(diamond)); @@ -170,6 +175,35 @@ contract RelayFacetTest is TestBaseFacet { vm.stopPrank(); } + function testRevert_WhenReplayingTransactionIds() public virtual { + relayFacet.setConsumedId(validRelayData.requestId); + bridgeData.receiver = LibAsset.NON_EVM_ADDRESS; + bridgeData.destinationChainId = 1151111081099710; + validRelayData = RelayFacet.RelayData({ + requestId: bytes32("1234"), + nonEVMReceiver: bytes32( + abi.encodePacked( + "EoW7FWTdPdZKpd3WAhH98c2HMGHsdh5yhzzEtk1u68Bb" + ) + ), // DEV Wallet + receivingAssetId: bytes32( + abi.encodePacked( + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + ) + ), // Solana USDC + signature: "" + }); + + vm.startPrank(USER_SENDER); + + // approval + usdc.approve(_facetTestContractAddress, bridgeData.minAmount); + + vm.expectRevert(InvalidQuote.selector); + initiateBridgeTxWithFacet(false); + vm.stopPrank(); + } + function test_CanBridgeNativeTokensToSolana() public virtual