diff --git a/.github/renovate.json b/.github/renovate.json
index 8ad2b13bca..f04adb7000 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json
@@ -17,6 +17,7 @@
"draftPR": true,
"cloneSubmodules": true,
"forkProcessing": "enabled",
+ "ignorePaths": ["packages/contracts-core/**"],
"packageRules": [
{
"matchUpdateTypes": [
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
index 9288fca1bc..5f7537182b 100644
--- a/.github/workflows/labeler.yml
+++ b/.github/workflows/labeler.yml
@@ -31,3 +31,10 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }} # Optional
filesizelimit: 15MB
+
+ - name: Add 'fe-release' label
+ if: github.event.pull_request.base.ref == 'fe-release'
+ uses: actions-ecosystem/action-add-labels@v1
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ labels: 'fe-release'
diff --git a/docs/bridge/CHANGELOG.md b/docs/bridge/CHANGELOG.md
index 60f5634ae4..73efed7d90 100644
--- a/docs/bridge/CHANGELOG.md
+++ b/docs/bridge/CHANGELOG.md
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [0.3.8](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.3.7...@synapsecns/bridge-docs@0.3.8) (2024-10-07)
+
+**Note:** Version bump only for package @synapsecns/bridge-docs
+
+
+
+
+
## [0.3.7](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.3.6...@synapsecns/bridge-docs@0.3.7) (2024-10-05)
**Note:** Version bump only for package @synapsecns/bridge-docs
diff --git a/docs/bridge/docs/01-About/03-Routes.md b/docs/bridge/docs/01-About/03-Routes.md
new file mode 100644
index 0000000000..5a6f844eb6
--- /dev/null
+++ b/docs/bridge/docs/01-About/03-Routes.md
@@ -0,0 +1,7 @@
+import Routes from '@site/src/components/Routes'
+
+# Chains & Tokens
+
+This page contains a list of supported tokens, listed per-chain. For a given pair, use the [Synapse Bridge](https://synapseprotocol.com) to see if a route between them exists.
+
+
diff --git a/docs/bridge/docs/02-Bridge/04-Sample-Code.md b/docs/bridge/docs/02-Bridge/04-Code-Examples.md
similarity index 99%
rename from docs/bridge/docs/02-Bridge/04-Sample-Code.md
rename to docs/bridge/docs/02-Bridge/04-Code-Examples.md
index ca625344a3..1816550110 100644
--- a/docs/bridge/docs/02-Bridge/04-Sample-Code.md
+++ b/docs/bridge/docs/02-Bridge/04-Code-Examples.md
@@ -1,8 +1,8 @@
---
-sidebar_label: Sample Code
+sidebar_label: Examples
---
-# Sample Code
+# Example Code
Example SDK & API implementations
diff --git a/docs/bridge/docs/02-Bridge/05-Supported-Routes.md b/docs/bridge/docs/02-Bridge/05-Supported-Routes.md
deleted file mode 100644
index f47a6918f2..0000000000
--- a/docs/bridge/docs/02-Bridge/05-Supported-Routes.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
-sidebar_label: Supported Rotues
----
-
-# Supported Routes
-
-Use the [Synapse Bridge](https://synapseprotocol.com) to browse supported tokens and chains.
-
-:::tip Routes
-
-Route availability is determined by the amount you wish to bridge. Use the [Synapse Bridge](https://synapseprotocol.com) to see if a route between a given pair exists.
-
-:::
-
diff --git a/docs/bridge/docs/02-Bridge/_05-Supported-Routes.md b/docs/bridge/docs/02-Bridge/_05-Supported-Routes.md
deleted file mode 100644
index 13bbab1dc7..0000000000
--- a/docs/bridge/docs/02-Bridge/_05-Supported-Routes.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-sidebar_label: Supported Rotues
----
-
-import Routes from '@site/src/components/Routes'
-
-# Supported Routes
-
-Supported tokens for each chain.
-
-:::tip Routes
-
-Route availability is determined by the amount you wish to bridge. Use the [Synapse Bridge](https://synapseprotocol.com) to see if a route between a given pair exists.
-
-:::
-
-
diff --git a/docs/bridge/docs/02-Bridge/index.md b/docs/bridge/docs/02-Bridge/index.md
index 5da83d208a..dfa2ba2519 100644
--- a/docs/bridge/docs/02-Bridge/index.md
+++ b/docs/bridge/docs/02-Bridge/index.md
@@ -7,7 +7,7 @@ import SVGBridge from '@site/src/components/SVGBridge'
# Synapse Bridge
-The [Synapse Bridge](https://synapseprotocol.com) and [Solana Bridge](https://solana.synapseprotocol.com/) seamlessly swap on-chain assets between [20+ EVM and non-EVM blockchains](./Supported-Routes) in a safe and secure manner.
+The [Synapse Bridge](https://synapseprotocol.com) and [Solana Bridge](https://solana.synapseprotocol.com/) seamlessly swap on-chain assets between [20+ EVM and non-EVM blockchains](/docs/About/Routes) in a safe and secure manner.
diff --git a/docs/bridge/package.json b/docs/bridge/package.json
index cf64b44003..6ec028a64d 100644
--- a/docs/bridge/package.json
+++ b/docs/bridge/package.json
@@ -1,6 +1,6 @@
{
"name": "@synapsecns/bridge-docs",
- "version": "0.3.7",
+ "version": "0.3.8",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
diff --git a/docs/bridge/src/components/Routes.tsx b/docs/bridge/src/components/Routes.tsx
index 02af8a2312..36e035fe3d 100644
--- a/docs/bridge/src/components/Routes.tsx
+++ b/docs/bridge/src/components/Routes.tsx
@@ -1,17 +1,10 @@
import { BRIDGABLE_TOKENS, CHAINS } from '@synapsecns/synapse-constants'
-const CHAINS_BY_ID = {}
-
-for (const { chainImg, id, name } of Object.values(CHAINS)) {
- if (id && name) {
- CHAINS_BY_ID[id] = { name, chainImg }
- }
-}
-
export default () =>
Object.entries(BRIDGABLE_TOKENS).map(([id, tokens]) => {
- const chain = CHAINS_BY_ID[id]
- const chainImg = chain.chainImg({ width: 28, height: 28 })
+ const chain = CHAINS.CHAINS_BY_ID[id]
+ const chainImg = chain.chainImg
+
return (
alignItems: 'center',
}}
>
- {chainImg} {chain.name} {id}
+
+ {chain.name} {id}
{Object.values(tokens).map((token) => {
- const tokenImg =
- typeof token.icon === 'string' ? (
-
- ) : (
- token.icon({ width: 16, height: 16 })
- )
-
return (
padding: '.25rem .5rem',
}}
>
- {tokenImg} {token.symbol}
+ {' '}
+ {token.symbol}
)
})}
diff --git a/packages/contracts-rfq/CHANGELOG.md b/packages/contracts-rfq/CHANGELOG.md
index 16614416b6..66d89dce04 100644
--- a/packages/contracts-rfq/CHANGELOG.md
+++ b/packages/contracts-rfq/CHANGELOG.md
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+# [0.7.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.6.2...@synapsecns/contracts-rfq@0.7.0) (2024-10-07)
+
+
+### Features
+
+* **contracts-rfq:** arbitrary calls without additional native value [SLT-233] ([#3215](https://github.com/synapsecns/sanguine/issues/3215)) ([6dc151c](https://github.com/synapsecns/sanguine/commit/6dc151c709970e2a891a56d10c8ffc67bdf95522))
+
+
+
+
+
## [0.6.2](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.6.1...@synapsecns/contracts-rfq@0.6.2) (2024-10-03)
diff --git a/packages/contracts-rfq/contracts/FastBridgeV2.sol b/packages/contracts-rfq/contracts/FastBridgeV2.sol
index 5a764a7c95..1a64515a33 100644
--- a/packages/contracts-rfq/contracts/FastBridgeV2.sol
+++ b/packages/contracts-rfq/contracts/FastBridgeV2.sol
@@ -2,6 +2,7 @@
pragma solidity 0.8.24;
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {UniversalTokenLib} from "./libs/UniversalToken.sol";
@@ -9,6 +10,7 @@ import {Admin} from "./Admin.sol";
import {IFastBridge} from "./interfaces/IFastBridge.sol";
import {IFastBridgeV2} from "./interfaces/IFastBridgeV2.sol";
import {IFastBridgeV2Errors} from "./interfaces/IFastBridgeV2Errors.sol";
+import {IFastBridgeRecipient} from "./interfaces/IFastBridgeRecipient.sol";
/// @notice FastBridgeV2 is a contract for bridging tokens across chains.
contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {
@@ -24,6 +26,9 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {
/// @notice Minimum deadline period to relay a requested bridge transaction
uint256 public constant MIN_DEADLINE_PERIOD = 30 minutes;
+ /// @notice Maximum length of accepted callParams
+ uint256 public constant MAX_CALL_PARAMS_LENGTH = 2 ** 16 - 1;
+
/// @notice Status of the bridge tx on origin chain
mapping(bytes32 => BridgeTxDetails) public bridgeTxDetails;
/// @notice Relay details on destination chain
@@ -45,7 +50,12 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {
function bridge(BridgeParams memory params) external payable {
bridge({
params: params,
- paramsV2: BridgeParamsV2({quoteRelayer: address(0), quoteExclusivitySeconds: 0, quoteId: bytes("")})
+ paramsV2: BridgeParamsV2({
+ quoteRelayer: address(0),
+ quoteExclusivitySeconds: 0,
+ quoteId: bytes(""),
+ callParams: bytes("")
+ })
});
}
@@ -117,6 +127,9 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {
/// @inheritdoc IFastBridge
function getBridgeTransaction(bytes memory request) external pure returns (BridgeTransaction memory) {
+ // TODO: the note below isn't true anymore with the BridgeTransactionV2 struct
+ // since the variable length `callParams` was added. This needs to be fixed/acknowledged.
+
// Note: when passing V2 request, this will decode the V1 fields correctly since the new fields were
// added as the last fields of the struct and hence the ABI decoder will simply ignore the extra data.
return abi.decode(request, (BridgeTransaction));
@@ -132,6 +145,7 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {
if (params.sender == address(0) || params.to == address(0)) revert ZeroAddress();
if (params.originToken == address(0) || params.destToken == address(0)) revert ZeroAddress();
if (params.deadline < block.timestamp + MIN_DEADLINE_PERIOD) revert DeadlineTooShort();
+ if (paramsV2.callParams.length > MAX_CALL_PARAMS_LENGTH) revert CallParamsLengthAboveMax();
int256 exclusivityEndTime = int256(block.timestamp) + paramsV2.quoteExclusivitySeconds;
// exclusivityEndTime must be in range (0 .. params.deadline]
if (exclusivityEndTime <= 0 || exclusivityEndTime > int256(params.deadline)) {
@@ -163,7 +177,8 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {
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)
+ exclusivityEndTime: uint256(exclusivityEndTime),
+ callParams: paramsV2.callParams
})
);
bytes32 transactionId = keccak256(request);
@@ -214,18 +229,32 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {
address token = transaction.destToken;
uint256 amount = transaction.destAmount;
- uint256 rebate = chainGasAmount;
- if (!transaction.sendChainGas) {
- // forward erc20
- rebate = 0;
+ // All state changes have been done at this point, can proceed to the external calls.
+ // This follows the checks-effects-interactions pattern to mitigate potential reentrancy attacks.
+ if (transaction.callParams.length == 0) {
+ // No arbitrary call requested, so we just pull the tokens from the Relayer to the recipient,
+ // or transfer ETH to the recipient (if token is ETH_ADDRESS)
_pullToken(to, token, amount);
- } else if (token == UniversalTokenLib.ETH_ADDRESS) {
- // lump in gas rebate into amount in native gas token
- _pullToken(to, token, amount + rebate);
- } else {
- // forward erc20 then forward gas rebate in native gas token
+ } else if (token != UniversalTokenLib.ETH_ADDRESS) {
+ // Arbitrary call requested with ERC20: pull the tokens from the Relayer to the recipient first
_pullToken(to, token, amount);
- _pullToken(to, UniversalTokenLib.ETH_ADDRESS, rebate);
+ // Follow up with the hook function call
+ _checkedCallRecipient({
+ recipient: to,
+ msgValue: 0,
+ token: token,
+ amount: amount,
+ callParams: transaction.callParams
+ });
+ } else {
+ // Arbitrary call requested with ETH: combine the ETH transfer with the call
+ _checkedCallRecipient({
+ recipient: to,
+ msgValue: amount,
+ token: token,
+ amount: amount,
+ callParams: transaction.callParams
+ });
}
emit BridgeRelayed(
@@ -237,7 +266,8 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {
transaction.destToken,
transaction.originAmount,
transaction.destAmount,
- rebate
+ // chainGasAmount is 0 since the gas rebate function is deprecated
+ 0
);
}
@@ -327,6 +357,31 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {
}
}
+ /// @notice Calls the Recipient's hook function with the specified callParams and performs
+ /// all the necessary checks for the returned value.
+ function _checkedCallRecipient(
+ address recipient,
+ uint256 msgValue,
+ address token,
+ uint256 amount,
+ bytes memory callParams
+ )
+ internal
+ {
+ bytes memory hookData =
+ abi.encodeCall(IFastBridgeRecipient.fastBridgeTransferReceived, (token, amount, callParams));
+ // This will bubble any revert messages from the hook function
+ bytes memory returnData = Address.functionCallWithValue({target: recipient, data: hookData, value: msgValue});
+ // Explicit revert if no return data at all
+ if (returnData.length == 0) revert RecipientNoReturnValue();
+ // Check that exactly a single return value was returned
+ if (returnData.length != 32) revert RecipientIncorrectReturnValue();
+ // Return value should be abi-encoded hook function selector
+ if (bytes32(returnData) != bytes32(IFastBridgeRecipient.fastBridgeTransferReceived.selector)) {
+ revert RecipientIncorrectReturnValue();
+ }
+ }
+
/// @notice Calculates time since proof submitted
/// @dev proof.timestamp stores casted uint40(block.timestamp) block timestamps for gas optimization
/// _timeSince(proof) can accomodate rollover case when block.timestamp > type(uint40).max but
diff --git a/packages/contracts-rfq/contracts/interfaces/IFastBridgeRecipient.sol b/packages/contracts-rfq/contracts/interfaces/IFastBridgeRecipient.sol
new file mode 100644
index 0000000000..e76ee865cd
--- /dev/null
+++ b/packages/contracts-rfq/contracts/interfaces/IFastBridgeRecipient.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+interface IFastBridgeRecipient {
+ function fastBridgeTransferReceived(
+ address token,
+ uint256 amount,
+ bytes memory callParams
+ )
+ external
+ payable
+ returns (bytes4);
+}
diff --git a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol
index 81a67636b5..c4c3751a5b 100644
--- a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol
+++ b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol
@@ -33,10 +33,12 @@ interface IFastBridgeV2 is IFastBridge {
/// @param quoteRelayer Relayer that provided the quote for the transaction
/// @param quoteExclusivitySeconds Period of time the quote relayer is guaranteed exclusivity after user's deposit
/// @param quoteId Unique quote identifier used for tracking the quote
+ /// @param callParams Parameters for the arbitrary call to the destination recipient (if any)
struct BridgeParamsV2 {
address quoteRelayer;
int256 quoteExclusivitySeconds;
bytes quoteId;
+ bytes callParams;
}
/// @notice Updated bridge transaction struct to include parameters introduced in FastBridgeV2.
@@ -57,6 +59,7 @@ interface IFastBridgeV2 is IFastBridge {
uint256 nonce;
address exclusivityRelayer;
uint256 exclusivityEndTime;
+ bytes callParams;
}
event BridgeQuoteDetails(bytes32 indexed transactionId, bytes quoteId);
diff --git a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2Errors.sol b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2Errors.sol
index 85f3bacdcf..7cc1423a84 100644
--- a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2Errors.sol
+++ b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2Errors.sol
@@ -3,6 +3,7 @@ pragma solidity ^0.8.0;
interface IFastBridgeV2Errors {
error AmountIncorrect();
+ error CallParamsLengthAboveMax();
error ChainIncorrect();
error ExclusivityParamsIncorrect();
error MsgValueIncorrect();
@@ -10,6 +11,9 @@ interface IFastBridgeV2Errors {
error StatusIncorrect();
error ZeroAddress();
+ error RecipientIncorrectReturnValue();
+ error RecipientNoReturnValue();
+
error DeadlineExceeded();
error DeadlineNotExceeded();
error DeadlineTooShort();
diff --git a/packages/contracts-rfq/package.json b/packages/contracts-rfq/package.json
index 288df494f0..83164f1f24 100644
--- a/packages/contracts-rfq/package.json
+++ b/packages/contracts-rfq/package.json
@@ -1,7 +1,7 @@
{
"name": "@synapsecns/contracts-rfq",
"license": "MIT",
- "version": "0.6.2",
+ "version": "0.7.0",
"description": "FastBridge contracts.",
"private": true,
"files": [
diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.ArbitraryCall.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.ArbitraryCall.t.sol
new file mode 100644
index 0000000000..af4f6235c6
--- /dev/null
+++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.ArbitraryCall.t.sol
@@ -0,0 +1,250 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import {FastBridgeV2DstExclusivityTest, IFastBridgeV2} from "./FastBridgeV2.Dst.Exclusivity.t.sol";
+import {RecipientMock} from "./mocks/RecipientMock.sol";
+
+import {Address} from "@openzeppelin/contracts/utils/Address.sol";
+
+// solhint-disable func-name-mixedcase, ordering
+contract FastBridgeV2DstArbitraryCallTest is FastBridgeV2DstExclusivityTest {
+ bytes public constant CALL_PARAMS = abi.encode("Hello, world!");
+ bytes public constant REVERT_MSG = "GM, this is a revert";
+
+ function createFixtures() public virtual override {
+ // In the inherited tests userB is always used as the recipient of the tokens.
+ userB = address(new RecipientMock());
+ vm.label(userB, "ContractRecipient");
+ super.createFixtures();
+ }
+
+ function createFixturesV2() public virtual override {
+ super.createFixturesV2();
+ setTokenTestCallParams(CALL_PARAMS);
+ setEthTestCallParams(CALL_PARAMS);
+ }
+
+ /// @notice We override the "expect event" function to also check for the arbitrary call
+ /// made to the token recipient.
+ function expectBridgeRelayed(
+ IFastBridgeV2.BridgeTransactionV2 memory bridgeTx,
+ bytes32 txId,
+ address relayer
+ )
+ public
+ virtual
+ override
+ {
+ vm.expectCall({callee: userB, data: getExpectedCalldata(bridgeTx), count: 1});
+ super.expectBridgeRelayed(bridgeTx, txId, relayer);
+ }
+
+ function mockRecipientRevert(IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) public {
+ vm.mockCallRevert({callee: userB, data: getExpectedCalldata(bridgeTx), revertData: bytes(REVERT_MSG)});
+ }
+
+ function getExpectedCalldata(IFastBridgeV2.BridgeTransactionV2 memory bridgeTx)
+ public
+ pure
+ returns (bytes memory)
+ {
+ // fastBridgeTransferReceived(token, amount, callParams)
+ return abi.encodeCall(
+ RecipientMock.fastBridgeTransferReceived, (bridgeTx.destToken, bridgeTx.destAmount, CALL_PARAMS)
+ );
+ }
+
+ // ═══════════════════════════════════════════════ RECIPIENT EOA ═══════════════════════════════════════════════════
+
+ function test_relay_token_revert_recipientNotContract() public {
+ setTokenTestRecipient(userA);
+ vm.expectRevert(abi.encodeWithSelector(Address.AddressEmptyCode.selector, userA));
+ relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx});
+ }
+
+ function test_relay_token_withRelayerAddress_revert_recipientNotContract() public {
+ setTokenTestRecipient(userA);
+ vm.expectRevert(abi.encodeWithSelector(Address.AddressEmptyCode.selector, userA));
+ relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx});
+ }
+
+ function test_relay_eth_revert_recipientNotContract() public {
+ setEthTestRecipient(userA);
+ vm.expectRevert(abi.encodeWithSelector(Address.AddressEmptyCode.selector, userA));
+ relay({caller: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ function test_relay_eth_withRelayerAddress_revert_recipientNotContract() public {
+ setEthTestRecipient(userA);
+ vm.expectRevert(abi.encodeWithSelector(Address.AddressEmptyCode.selector, userA));
+ relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ // ═════════════════════════════════════ EXCESSIVE RETURN VALUE RECIPIENT ══════════════════════════════════════════
+
+ function test_relay_token_excessiveReturnValueRecipient_revertWhenCallParamsPresent() public virtual override {
+ setTokenTestRecipient(excessiveReturnValueRecipient);
+ vm.expectRevert(RecipientIncorrectReturnValue.selector);
+ relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx});
+ }
+
+ function test_relay_token_withRelayerAddress_excessiveReturnValueRecipient_revertWhenCallParamsPresent()
+ public
+ virtual
+ override
+ {
+ setTokenTestRecipient(excessiveReturnValueRecipient);
+ vm.expectRevert(RecipientIncorrectReturnValue.selector);
+ relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx});
+ }
+
+ function test_relay_eth_excessiveReturnValueRecipient_revertWhenCallParamsPresent() public virtual override {
+ setEthTestRecipient(excessiveReturnValueRecipient);
+ vm.expectRevert(RecipientIncorrectReturnValue.selector);
+ relay({caller: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ function test_relay_eth_withRelayerAddress_excessiveReturnValueRecipient_revertWhenCallParamsPresent()
+ public
+ virtual
+ override
+ {
+ setEthTestRecipient(excessiveReturnValueRecipient);
+ vm.expectRevert(RecipientIncorrectReturnValue.selector);
+ relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ // ═════════════════════════════════════ INCORRECT RETURN VALUE RECIPIENT ══════════════════════════════════════════
+
+ function test_relay_token_incorrectReturnValueRecipient_revertWhenCallParamsPresent() public virtual override {
+ setTokenTestRecipient(incorrectReturnValueRecipient);
+ vm.expectRevert(RecipientIncorrectReturnValue.selector);
+ relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx});
+ }
+
+ function test_relay_token_withRelayerAddress_incorrectReturnValueRecipient_revertWhenCallParamsPresent()
+ public
+ virtual
+ override
+ {
+ setTokenTestRecipient(incorrectReturnValueRecipient);
+ vm.expectRevert(RecipientIncorrectReturnValue.selector);
+ relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx});
+ }
+
+ function test_relay_eth_incorrectReturnValueRecipient_revertWhenCallParamsPresent() public virtual override {
+ setEthTestRecipient(incorrectReturnValueRecipient);
+ vm.expectRevert(RecipientIncorrectReturnValue.selector);
+ relay({caller: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ function test_relay_eth_withRelayerAddress_incorrectReturnValueRecipient_revertWhenCallParamsPresent()
+ public
+ virtual
+ override
+ {
+ setEthTestRecipient(incorrectReturnValueRecipient);
+ vm.expectRevert(RecipientIncorrectReturnValue.selector);
+ relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ // ══════════════════════════════════════════════ NO-OP RECIPIENT ══════════════════════════════════════════════════
+
+ function test_relay_token_noOpRecipient_revertWhenCallParamsPresent() public virtual override {
+ setTokenTestRecipient(noOpRecipient);
+ vm.expectRevert(Address.FailedInnerCall.selector);
+ relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx});
+ }
+
+ function test_relay_token_withRelayerAddress_noOpRecipient_revertWhenCallParamsPresent() public virtual override {
+ setTokenTestRecipient(noOpRecipient);
+ vm.expectRevert(Address.FailedInnerCall.selector);
+ relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx});
+ }
+
+ function test_relay_eth_noOpRecipient_revertWhenCallParamsPresent() public virtual override {
+ setEthTestRecipient(noOpRecipient);
+ vm.expectRevert(Address.FailedInnerCall.selector);
+ relay({caller: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ function test_relay_eth_withRelayerAddress_noOpRecipient_revertWhenCallParamsPresent() public virtual override {
+ setEthTestRecipient(noOpRecipient);
+ vm.expectRevert(Address.FailedInnerCall.selector);
+ relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ // ═════════════════════════════════════════ NO RETURN VALUE RECIPIENT ═════════════════════════════════════════════
+
+ function test_relay_token_noReturnValueRecipient_revertWhenCallParamsPresent() public virtual override {
+ setTokenTestRecipient(noReturnValueRecipient);
+ vm.expectRevert(RecipientNoReturnValue.selector);
+ relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx});
+ }
+
+ function test_relay_token_withRelayerAddress_noReturnValueRecipient_revertWhenCallParamsPresent()
+ public
+ virtual
+ override
+ {
+ setTokenTestRecipient(noReturnValueRecipient);
+ vm.expectRevert(RecipientNoReturnValue.selector);
+ relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx});
+ }
+
+ function test_relay_eth_noReturnValueRecipient_revertWhenCallParamsPresent() public virtual override {
+ setEthTestRecipient(noReturnValueRecipient);
+ vm.expectRevert(RecipientNoReturnValue.selector);
+ relay({caller: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ function test_relay_eth_withRelayerAddress_noReturnValueRecipient_revertWhenCallParamsPresent()
+ public
+ virtual
+ override
+ {
+ setEthTestRecipient(noReturnValueRecipient);
+ vm.expectRevert(RecipientNoReturnValue.selector);
+ relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ // ═════════════════════════════════════════════ RECIPIENT REVERTS ═════════════════════════════════════════════════
+
+ function test_relay_token_revert_recipientReverts() public {
+ mockRecipientRevert(tokenTx);
+ vm.expectRevert(REVERT_MSG);
+ relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx});
+ }
+
+ function test_relay_token_withRelayerAddress_revert_recipientReverts() public {
+ mockRecipientRevert(tokenTx);
+ vm.expectRevert(REVERT_MSG);
+ relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx});
+ }
+
+ function test_relay_eth_revert_recipientReverts() public {
+ mockRecipientRevert(ethTx);
+ vm.expectRevert(REVERT_MSG);
+ relay({caller: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ function test_relay_eth_withRelayerAddress_revert_recipientReverts() public {
+ mockRecipientRevert(ethTx);
+ vm.expectRevert(REVERT_MSG);
+ relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ function test_relay_eth_noCallParams_revert_recipientReverts() public {
+ setEthTestCallParams("");
+ vm.mockCallRevert({callee: userB, data: "", revertData: bytes(REVERT_MSG)});
+ vm.expectRevert("ETH transfer failed");
+ relay({caller: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ function test_relay_eth_withRelayerAddress_noCallParams_revert_recipientReverts() public {
+ setEthTestCallParams("");
+ vm.mockCallRevert({callee: userB, data: "", revertData: bytes(REVERT_MSG)});
+ vm.expectRevert("ETH transfer failed");
+ relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+}
diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.Exclusivity.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.Exclusivity.t.sol
index 9d0a414306..a616872f93 100644
--- a/packages/contracts-rfq/test/FastBridgeV2.Dst.Exclusivity.t.sol
+++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.Exclusivity.t.sol
@@ -11,12 +11,14 @@ contract FastBridgeV2DstExclusivityTest is FastBridgeV2DstTest {
tokenParamsV2 = IFastBridgeV2.BridgeParamsV2({
quoteRelayer: relayerA,
quoteExclusivitySeconds: int256(EXCLUSIVITY_PERIOD),
- quoteId: ""
+ quoteId: "",
+ callParams: ""
});
ethParamsV2 = IFastBridgeV2.BridgeParamsV2({
quoteRelayer: relayerB,
quoteExclusivitySeconds: int256(EXCLUSIVITY_PERIOD),
- quoteId: ""
+ quoteId: "",
+ callParams: ""
});
tokenTx.exclusivityRelayer = relayerA;
diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol
index e441a9f3c4..89ef62af91 100644
--- a/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol
+++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol
@@ -3,6 +3,12 @@ pragma solidity ^0.8.20;
import {FastBridgeV2DstBaseTest, IFastBridgeV2} from "./FastBridgeV2.Dst.Base.t.sol";
+import {ExcessiveReturnValueRecipient} from "./mocks/ExcessiveReturnValueRecipient.sol";
+import {IncorrectReturnValueRecipient} from "./mocks/IncorrectReturnValueRecipient.sol";
+import {NoOpContract} from "./mocks/NoOpContract.sol";
+import {NoReturnValueRecipient} from "./mocks/NoReturnValueRecipient.sol";
+import {NonPayableRecipient} from "./mocks/NonPayableRecipient.sol";
+
// solhint-disable func-name-mixedcase, ordering
contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest {
event BridgeRelayed(
@@ -17,12 +23,49 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest {
uint256 chainGasAmount
);
+ address public excessiveReturnValueRecipient;
+ address public incorrectReturnValueRecipient;
+ address public noOpRecipient;
+ address public noReturnValueRecipient;
+ address public nonPayableRecipient;
+
+ function setUp() public virtual override {
+ super.setUp();
+ excessiveReturnValueRecipient = address(new ExcessiveReturnValueRecipient());
+ vm.label(excessiveReturnValueRecipient, "ExcessiveReturnValueRecipient");
+ incorrectReturnValueRecipient = address(new IncorrectReturnValueRecipient());
+ vm.label(incorrectReturnValueRecipient, "IncorrectReturnValueRecipient");
+ noOpRecipient = address(new NoOpContract());
+ vm.label(noOpRecipient, "NoOpRecipient");
+ noReturnValueRecipient = address(new NoReturnValueRecipient());
+ vm.label(noReturnValueRecipient, "NoReturnValueRecipient");
+ nonPayableRecipient = address(new NonPayableRecipient());
+ vm.label(nonPayableRecipient, "NonPayableRecipient");
+ }
+
+ function setTokenTestRecipient(address recipient) public {
+ userB = recipient;
+ tokenParams.to = recipient;
+ tokenTx.destRecipient = recipient;
+ }
+
+ function setEthTestRecipient(address recipient) public {
+ userB = recipient;
+ ethParams.to = recipient;
+ ethTx.destRecipient = recipient;
+ }
+
+ function assertEmptyCallParams(bytes memory callParams) public pure {
+ assertEq(callParams.length, 0, "Invalid setup: callParams are not empty");
+ }
+
function expectBridgeRelayed(
IFastBridgeV2.BridgeTransactionV2 memory bridgeTx,
bytes32 txId,
address relayer
)
public
+ virtual
{
vm.expectEmit(address(fastBridge));
emit BridgeRelayed({
@@ -107,6 +150,160 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest {
assertEq(relayerA.balance, LEFTOVER_BALANCE);
assertEq(address(fastBridge).balance, 0);
}
+
+ // ═════════════════════════════════════ EXCESSIVE RETURN VALUE RECIPIENT ══════════════════════════════════════════
+
+ // Note: in this test, the callParams are not present, and the below test functions succeed.
+ // Override them in the derived tests where callParams are present to check for a revert.
+
+ function test_relay_token_excessiveReturnValueRecipient_revertWhenCallParamsPresent() public virtual {
+ assertEmptyCallParams(tokenTx.callParams);
+ setTokenTestRecipient(excessiveReturnValueRecipient);
+ test_relay_token();
+ }
+
+ function test_relay_token_withRelayerAddress_excessiveReturnValueRecipient_revertWhenCallParamsPresent()
+ public
+ virtual
+ {
+ assertEmptyCallParams(tokenTx.callParams);
+ setTokenTestRecipient(excessiveReturnValueRecipient);
+ test_relay_token_withRelayerAddress();
+ }
+
+ function test_relay_eth_excessiveReturnValueRecipient_revertWhenCallParamsPresent() public virtual {
+ assertEmptyCallParams(ethTx.callParams);
+ setEthTestRecipient(excessiveReturnValueRecipient);
+ test_relay_eth();
+ }
+
+ function test_relay_eth_withRelayerAddress_excessiveReturnValueRecipient_revertWhenCallParamsPresent()
+ public
+ virtual
+ {
+ assertEmptyCallParams(ethTx.callParams);
+ setEthTestRecipient(excessiveReturnValueRecipient);
+ test_relay_eth_withRelayerAddress();
+ }
+
+ // ═════════════════════════════════════ INCORRECT RETURN VALUE RECIPIENT ══════════════════════════════════════════
+
+ // Note: in this test, the callParams are not present, and the below test functions succeed.
+ // Override them in the derived tests where callParams are present to check for a revert.
+
+ function test_relay_token_incorrectReturnValueRecipient_revertWhenCallParamsPresent() public virtual {
+ assertEmptyCallParams(tokenTx.callParams);
+ setTokenTestRecipient(incorrectReturnValueRecipient);
+ test_relay_token();
+ }
+
+ function test_relay_token_withRelayerAddress_incorrectReturnValueRecipient_revertWhenCallParamsPresent()
+ public
+ virtual
+ {
+ assertEmptyCallParams(tokenTx.callParams);
+ setTokenTestRecipient(incorrectReturnValueRecipient);
+ test_relay_token_withRelayerAddress();
+ }
+
+ function test_relay_eth_incorrectReturnValueRecipient_revertWhenCallParamsPresent() public virtual {
+ assertEmptyCallParams(ethTx.callParams);
+ setEthTestRecipient(incorrectReturnValueRecipient);
+ test_relay_eth();
+ }
+
+ function test_relay_eth_withRelayerAddress_incorrectReturnValueRecipient_revertWhenCallParamsPresent()
+ public
+ virtual
+ {
+ assertEmptyCallParams(ethTx.callParams);
+ setEthTestRecipient(incorrectReturnValueRecipient);
+ test_relay_eth_withRelayerAddress();
+ }
+
+ // ═══════════════════════════════════════════ NON PAYABLE RECIPIENT ═══════════════════════════════════════════════
+
+ /// @notice Should not affect the ERC20 transfer
+ function test_relay_token_nonPayableRecipient() public {
+ setTokenTestRecipient(nonPayableRecipient);
+ test_relay_token();
+ }
+
+ function test_relay_token_withRelayerAddress_nonPayableRecipient() public {
+ setTokenTestRecipient(nonPayableRecipient);
+ test_relay_token_withRelayerAddress();
+ }
+
+ function test_relay_eth_revert_nonPayableRecipient() public {
+ setEthTestRecipient(nonPayableRecipient);
+ vm.expectRevert();
+ relay({caller: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ function test_relay_eth_withRelayerAddress_revert_nonPayableRecipient() public {
+ setEthTestRecipient(nonPayableRecipient);
+ vm.expectRevert();
+ relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx});
+ }
+
+ // ══════════════════════════════════════════════ NO-OP RECIPIENT ══════════════════════════════════════════════════
+
+ // Note: in this test, the callParams are not present, and the below test functions succeed.
+ // Override them in the derived tests where callParams are present to check for a revert.
+
+ function test_relay_token_noOpRecipient_revertWhenCallParamsPresent() public virtual {
+ assertEmptyCallParams(tokenTx.callParams);
+ setTokenTestRecipient(noOpRecipient);
+ test_relay_token();
+ }
+
+ function test_relay_token_withRelayerAddress_noOpRecipient_revertWhenCallParamsPresent() public virtual {
+ assertEmptyCallParams(tokenTx.callParams);
+ setTokenTestRecipient(noOpRecipient);
+ test_relay_token_withRelayerAddress();
+ }
+
+ function test_relay_eth_noOpRecipient_revertWhenCallParamsPresent() public virtual {
+ assertEmptyCallParams(ethTx.callParams);
+ setEthTestRecipient(noOpRecipient);
+ test_relay_eth();
+ }
+
+ function test_relay_eth_withRelayerAddress_noOpRecipient_revertWhenCallParamsPresent() public virtual {
+ assertEmptyCallParams(ethTx.callParams);
+ setEthTestRecipient(noOpRecipient);
+ test_relay_eth_withRelayerAddress();
+ }
+
+ // ═════════════════════════════════════════ NO RETURN VALUE RECIPIENT ═════════════════════════════════════════════
+
+ // Note: in this test, the callParams are not present, and the below test functions succeed.
+ // Override them in the derived tests where callParams are present to check for a revert.
+
+ function test_relay_token_noReturnValueRecipient_revertWhenCallParamsPresent() public virtual {
+ assertEmptyCallParams(tokenTx.callParams);
+ setTokenTestRecipient(noReturnValueRecipient);
+ test_relay_token();
+ }
+
+ function test_relay_token_withRelayerAddress_noReturnValueRecipient_revertWhenCallParamsPresent() public virtual {
+ assertEmptyCallParams(tokenTx.callParams);
+ setTokenTestRecipient(noReturnValueRecipient);
+ test_relay_token_withRelayerAddress();
+ }
+
+ function test_relay_eth_noReturnValueRecipient_revertWhenCallParamsPresent() public virtual {
+ assertEmptyCallParams(ethTx.callParams);
+ setEthTestRecipient(noReturnValueRecipient);
+ test_relay_eth();
+ }
+
+ function test_relay_eth_withRelayerAddress_noReturnValueRecipient_revertWhenCallParamsPresent() public virtual {
+ assertEmptyCallParams(ethTx.callParams);
+ setEthTestRecipient(noReturnValueRecipient);
+ test_relay_eth_withRelayerAddress();
+ }
+
// ══════════════════════════════════════════════════ REVERTS ══════════════════════════════════════════════════════
function test_relay_revert_usedRequestV1() public {
diff --git a/packages/contracts-rfq/test/FastBridgeV2.Encoding.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Encoding.t.sol
index 58123fad28..0918c66bf5 100644
--- a/packages/contracts-rfq/test/FastBridgeV2.Encoding.t.sol
+++ b/packages/contracts-rfq/test/FastBridgeV2.Encoding.t.sol
@@ -47,7 +47,14 @@ contract FastBridgeV2EncodingTest is FastBridgeV2Test {
assertEq(decodedTx, bridgeTx);
}
- function test_getBridgeTransaction_supportsV2(IFastBridgeV2.BridgeTransactionV2 memory bridgeTxV2) public view {
+ // The addition of variable length field (callParams) in BridgeTransactionV2 breaks the compatibility
+ // with the original BridgeTransaction struct.
+ // Solidity's abi.encode(bridgeTxV2) will use the first 32 bytes to encode the data offset for the whole struct,
+ // which is ALWAYS equal to 32 (data starts right after the offset). This is weird, but it is what it is.
+ // https://ethereum.stackexchange.com/questions/152971/abi-encode-decode-mystery-additional-32-byte-field-uniswap-v2
+ function test_getBridgeTransaction_supportsV2(IFastBridgeV2.BridgeTransactionV2 memory bridgeTxV2) public {
+ // TODO: reevaluate the necessity of this test if/when the encoding scheme is changed
+ vm.skip(true);
bytes memory request = abi.encode(bridgeTxV2);
IFastBridge.BridgeTransaction memory decodedTx = fastBridge.getBridgeTransaction(request);
assertEq(decodedTx, extractV1(bridgeTxV2));
diff --git a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.Excl.t.sol b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.Excl.t.sol
index dc6b5953f5..eae0dffaa1 100644
--- a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.Excl.t.sol
+++ b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.Excl.t.sol
@@ -13,13 +13,7 @@ contract FastBridgeV2DstExclusivityTest is FastBridgeV2DstGasBenchmarkTest {
}
function createFixturesV2() public virtual override {
- tokenParamsV2.quoteRelayer = relayerA;
- tokenParamsV2.quoteExclusivitySeconds = int256(EXCLUSIVITY_PERIOD);
- ethParamsV2.quoteRelayer = relayerA;
- ethParamsV2.quoteExclusivitySeconds = int256(EXCLUSIVITY_PERIOD);
- tokenTx.exclusivityRelayer = relayerA;
- tokenTx.exclusivityEndTime = block.timestamp + EXCLUSIVITY_PERIOD;
- ethTx.exclusivityRelayer = relayerA;
- ethTx.exclusivityEndTime = block.timestamp + EXCLUSIVITY_PERIOD;
+ setTokenTestExclusivityParams(relayerA, EXCLUSIVITY_PERIOD);
+ setEthTestExclusivityParams(relayerA, EXCLUSIVITY_PERIOD);
}
}
diff --git a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.t.sol b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.t.sol
index 0d6314baac..0b6834d6ff 100644
--- a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.t.sol
+++ b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.t.sol
@@ -104,11 +104,7 @@ contract FastBridgeV2GasBenchmarkSrcTest is FastBridgeV2SrcBaseTest {
}
function test_bridge_token_withExclusivity() public {
- tokenParamsV2.quoteRelayer = relayerA;
- tokenParamsV2.quoteExclusivitySeconds = int256(EXCLUSIVITY_PERIOD);
- tokenParamsV2.quoteId = bytes("Created by Relayer A");
- tokenTx.exclusivityRelayer = relayerA;
- tokenTx.exclusivityEndTime = block.timestamp + EXCLUSIVITY_PERIOD;
+ setTokenTestExclusivityParams(relayerA, EXCLUSIVITY_PERIOD);
bridge({caller: userA, msgValue: 0, params: tokenParams, paramsV2: tokenParamsV2});
assertEq(fastBridge.bridgeStatuses(getTxId(tokenTx)), IFastBridgeV2.BridgeStatus.REQUESTED);
assertEq(srcToken.balanceOf(userA), initialUserBalanceToken - tokenParams.originAmount);
@@ -189,11 +185,7 @@ contract FastBridgeV2GasBenchmarkSrcTest is FastBridgeV2SrcBaseTest {
}
function test_bridge_eth_withExclusivity() public {
- ethParamsV2.quoteRelayer = relayerA;
- ethParamsV2.quoteExclusivitySeconds = int256(EXCLUSIVITY_PERIOD);
- ethParamsV2.quoteId = bytes("Created by Relayer A");
- ethTx.exclusivityRelayer = relayerA;
- ethTx.exclusivityEndTime = block.timestamp + EXCLUSIVITY_PERIOD;
+ setEthTestExclusivityParams(relayerA, EXCLUSIVITY_PERIOD);
bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams, paramsV2: ethParamsV2});
assertEq(fastBridge.bridgeStatuses(getTxId(ethTx)), IFastBridgeV2.BridgeStatus.REQUESTED);
assertEq(userA.balance, initialUserBalanceEth - ethParams.originAmount);
diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.ArbitraryCall.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.ArbitraryCall.t.sol
new file mode 100644
index 0000000000..2b1e83a30c
--- /dev/null
+++ b/packages/contracts-rfq/test/FastBridgeV2.Src.ArbitraryCall.t.sol
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import {FastBridgeV2SrcExclusivityTest} from "./FastBridgeV2.Src.Exclusivity.t.sol";
+
+// solhint-disable func-name-mixedcase, ordering
+contract FastBridgeV2SrcArbitraryCallTest is FastBridgeV2SrcExclusivityTest {
+ bytes public constant CALL_PARAMS = abi.encode("Hello, World!");
+
+ function createFixturesV2() public virtual override {
+ super.createFixturesV2();
+ setTokenTestCallParams(CALL_PARAMS);
+ setEthTestCallParams(CALL_PARAMS);
+ }
+
+ // Contract should accept callParams with length up to 2^16 - 1,
+ // so that the callParams.length is encoded in 2 bytes.
+
+ function test_bridge_token_callParamsLengthMax() public {
+ bytes memory callParams = new bytes(2 ** 16 - 1);
+ setTokenTestCallParams(callParams);
+ test_bridge_token();
+ }
+
+ function test_bridge_eth_callParamsLengthMax() public {
+ bytes memory callParams = new bytes(2 ** 16 - 1);
+ setEthTestCallParams(callParams);
+ test_bridge_eth();
+ }
+
+ function test_bridge_token_revert_callParamsLengthAboveMax() public {
+ bytes memory callParams = new bytes(2 ** 16);
+ setTokenTestCallParams(callParams);
+ vm.expectRevert(CallParamsLengthAboveMax.selector);
+ bridge({caller: userA, msgValue: 0, params: tokenParams, paramsV2: tokenParamsV2});
+ }
+
+ function test_bridge_eth_revert_callParamsLengthAboveMax() public {
+ bytes memory callParams = new bytes(2 ** 16);
+ setEthTestCallParams(callParams);
+ vm.expectRevert(CallParamsLengthAboveMax.selector);
+ bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams, paramsV2: ethParamsV2});
+ }
+}
diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.Exclusivity.Negative.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.Exclusivity.Negative.t.sol
index 0b19f097f3..4eeacb2f88 100644
--- a/packages/contracts-rfq/test/FastBridgeV2.Src.Exclusivity.Negative.t.sol
+++ b/packages/contracts-rfq/test/FastBridgeV2.Src.Exclusivity.Negative.t.sol
@@ -8,16 +8,13 @@ contract FastBridgeV2SrcExclusivityNegativeTest is FastBridgeV2SrcTest {
uint256 public constant EXCLUSIVITY_PERIOD_ABS = 60 seconds;
function createFixturesV2() public virtual override {
- tokenParamsV2.quoteRelayer = relayerA;
+ // Populate the fields using the absolute exclusivity period
+ setTokenTestExclusivityParams(relayerA, EXCLUSIVITY_PERIOD_ABS);
+ setEthTestExclusivityParams(relayerB, EXCLUSIVITY_PERIOD_ABS);
+ // Override with negative exclusivity period
tokenParamsV2.quoteExclusivitySeconds = -int256(EXCLUSIVITY_PERIOD_ABS);
- tokenParamsV2.quoteId = bytes("Created by Relayer A");
- ethParamsV2.quoteRelayer = relayerB;
ethParamsV2.quoteExclusivitySeconds = -int256(EXCLUSIVITY_PERIOD_ABS);
- ethParamsV2.quoteId = bytes("Created by Relayer B");
-
- tokenTx.exclusivityRelayer = relayerA;
tokenTx.exclusivityEndTime = block.timestamp - EXCLUSIVITY_PERIOD_ABS;
- ethTx.exclusivityRelayer = relayerB;
ethTx.exclusivityEndTime = block.timestamp - EXCLUSIVITY_PERIOD_ABS;
}
diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.Exclusivity.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.Exclusivity.t.sol
index 259ef0bacd..230c24778c 100644
--- a/packages/contracts-rfq/test/FastBridgeV2.Src.Exclusivity.t.sol
+++ b/packages/contracts-rfq/test/FastBridgeV2.Src.Exclusivity.t.sol
@@ -8,17 +8,8 @@ contract FastBridgeV2SrcExclusivityTest is FastBridgeV2SrcTest {
uint256 public constant EXCLUSIVITY_PERIOD = 60 seconds;
function createFixturesV2() public virtual override {
- tokenParamsV2.quoteRelayer = relayerA;
- tokenParamsV2.quoteExclusivitySeconds = int256(EXCLUSIVITY_PERIOD);
- tokenParamsV2.quoteId = bytes("Created by Relayer A");
- ethParamsV2.quoteRelayer = relayerB;
- ethParamsV2.quoteExclusivitySeconds = int256(EXCLUSIVITY_PERIOD);
- ethParamsV2.quoteId = bytes("Created by Relayer B");
-
- tokenTx.exclusivityRelayer = relayerA;
- tokenTx.exclusivityEndTime = block.timestamp + EXCLUSIVITY_PERIOD;
- ethTx.exclusivityRelayer = relayerB;
- ethTx.exclusivityEndTime = block.timestamp + EXCLUSIVITY_PERIOD;
+ setTokenTestExclusivityParams(relayerA, EXCLUSIVITY_PERIOD);
+ setEthTestExclusivityParams(relayerB, EXCLUSIVITY_PERIOD);
}
function bridge(address caller, uint256 msgValue, IFastBridge.BridgeParams memory params) public virtual override {
diff --git a/packages/contracts-rfq/test/FastBridgeV2.t.sol b/packages/contracts-rfq/test/FastBridgeV2.t.sol
index 3828076dac..8011f8a8e2 100644
--- a/packages/contracts-rfq/test/FastBridgeV2.t.sol
+++ b/packages/contracts-rfq/test/FastBridgeV2.t.sol
@@ -124,9 +124,18 @@ abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors {
function createFixturesV2() public virtual {
// Override in tests with exclusivity params
- tokenParamsV2 =
- IFastBridgeV2.BridgeParamsV2({quoteRelayer: address(0), quoteExclusivitySeconds: 0, quoteId: ""});
- ethParamsV2 = IFastBridgeV2.BridgeParamsV2({quoteRelayer: address(0), quoteExclusivitySeconds: 0, quoteId: ""});
+ tokenParamsV2 = IFastBridgeV2.BridgeParamsV2({
+ quoteRelayer: address(0),
+ quoteExclusivitySeconds: 0,
+ quoteId: bytes(""),
+ callParams: bytes("")
+ });
+ ethParamsV2 = IFastBridgeV2.BridgeParamsV2({
+ quoteRelayer: address(0),
+ quoteExclusivitySeconds: 0,
+ quoteId: bytes(""),
+ callParams: bytes("")
+ });
tokenTx.exclusivityRelayer = address(0);
tokenTx.exclusivityEndTime = block.timestamp;
@@ -154,6 +163,34 @@ abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors {
txV2.nonce = txV1.nonce;
}
+ function setTokenTestCallParams(bytes memory callParams) public {
+ tokenParamsV2.callParams = callParams;
+ tokenTx.callParams = callParams;
+ }
+
+ function setTokenTestExclusivityParams(address relayer, uint256 exclusivitySeconds) public {
+ tokenParamsV2.quoteRelayer = relayer;
+ tokenParamsV2.quoteExclusivitySeconds = int256(exclusivitySeconds);
+ tokenParamsV2.quoteId = bytes.concat("Token:", getMockQuoteId(relayer));
+
+ tokenTx.exclusivityRelayer = relayer;
+ tokenTx.exclusivityEndTime = block.timestamp + exclusivitySeconds;
+ }
+
+ function setEthTestCallParams(bytes memory callParams) public {
+ ethParamsV2.callParams = callParams;
+ ethTx.callParams = callParams;
+ }
+
+ function setEthTestExclusivityParams(address relayer, uint256 exclusivitySeconds) public {
+ ethParamsV2.quoteRelayer = relayer;
+ ethParamsV2.quoteExclusivitySeconds = int256(exclusivitySeconds);
+ ethParamsV2.quoteId = bytes.concat("ETH:", getMockQuoteId(relayer));
+
+ ethTx.exclusivityRelayer = relayer;
+ ethTx.exclusivityEndTime = block.timestamp + exclusivitySeconds;
+ }
+
function extractV1(IFastBridgeV2.BridgeTransactionV2 memory txV2)
public
pure
@@ -173,6 +210,16 @@ abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors {
txV1.nonce = txV2.nonce;
}
+ function getMockQuoteId(address relayer) public view returns (bytes memory) {
+ if (relayer == relayerA) {
+ return bytes("created by Relayer A");
+ } else if (relayer == relayerB) {
+ return bytes("created by Relayer B");
+ } else {
+ return bytes("created by unknown relayer");
+ }
+ }
+
function getTxId(IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) public pure returns (bytes32) {
return keccak256(abi.encode(bridgeTx));
}
diff --git a/packages/contracts-rfq/test/mocks/ExcessiveReturnValueRecipient.sol b/packages/contracts-rfq/test/mocks/ExcessiveReturnValueRecipient.sol
new file mode 100644
index 0000000000..9c3be02502
--- /dev/null
+++ b/packages/contracts-rfq/test/mocks/ExcessiveReturnValueRecipient.sol
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import {IFastBridgeRecipient} from "../../contracts/interfaces/IFastBridgeRecipient.sol";
+
+/// @notice Incorrectly implemented recipient mock for testing purposes. DO NOT USE IN PRODUCTION.
+contract ExcessiveReturnValueRecipient {
+ /// @notice Mock needs to accept ETH
+ receive() external payable {}
+
+ /// @notice Incorrectly implemented - method returns excessive bytes.
+ function fastBridgeTransferReceived(address, uint256, bytes memory) external payable returns (bytes4, uint256) {
+ return (IFastBridgeRecipient.fastBridgeTransferReceived.selector, 1337);
+ }
+}
diff --git a/packages/contracts-rfq/test/mocks/IncorrectReturnValueRecipient.sol b/packages/contracts-rfq/test/mocks/IncorrectReturnValueRecipient.sol
new file mode 100644
index 0000000000..2bf955da7f
--- /dev/null
+++ b/packages/contracts-rfq/test/mocks/IncorrectReturnValueRecipient.sol
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import {IFastBridgeRecipient} from "../../contracts/interfaces/IFastBridgeRecipient.sol";
+
+/// @notice Incorrectly implemented recipient mock for testing purposes. DO NOT USE IN PRODUCTION.
+contract IncorrectReturnValueRecipient is IFastBridgeRecipient {
+ /// @notice Mock needs to accept ETH
+ receive() external payable {}
+
+ /// @notice Incorrectly implemented - method returns incorrect value.
+ function fastBridgeTransferReceived(address, uint256, bytes memory) external payable returns (bytes4) {
+ // Flip the last bit
+ return IFastBridgeRecipient.fastBridgeTransferReceived.selector ^ 0x00000001;
+ }
+}
diff --git a/packages/contracts-rfq/test/mocks/NoOpContract.sol b/packages/contracts-rfq/test/mocks/NoOpContract.sol
new file mode 100644
index 0000000000..ae66cfbcce
--- /dev/null
+++ b/packages/contracts-rfq/test/mocks/NoOpContract.sol
@@ -0,0 +1,8 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+// solhint-disable-next-line no-empty-blocks
+contract NoOpContract {
+ /// @notice Mock needs to accept ETH
+ receive() external payable {}
+}
diff --git a/packages/contracts-rfq/test/mocks/NoReturnValueRecipient.sol b/packages/contracts-rfq/test/mocks/NoReturnValueRecipient.sol
new file mode 100644
index 0000000000..e10c8b6ded
--- /dev/null
+++ b/packages/contracts-rfq/test/mocks/NoReturnValueRecipient.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+// solhint-disable
+
+/// @notice Incorrectly implemented recipient mock for testing purposes. DO NOT USE IN PRODUCTION.
+contract NoReturnValueRecipient {
+ /// @notice Mock needs to accept ETH
+ receive() external payable {}
+
+ /// @notice Incorrectly implemented - method does not return anything.
+ function fastBridgeTransferReceived(address, uint256, bytes memory) external payable {}
+}
diff --git a/packages/contracts-rfq/test/mocks/NonPayableRecipient.sol b/packages/contracts-rfq/test/mocks/NonPayableRecipient.sol
new file mode 100644
index 0000000000..1f53dabfd1
--- /dev/null
+++ b/packages/contracts-rfq/test/mocks/NonPayableRecipient.sol
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+/// @notice Incorrectly implemented recipient mock for testing purposes. DO NOT USE IN PRODUCTION.
+contract NonPayableRecipient {
+ /// @notice Incorrectly implemented - method is not payable.
+ function fastBridgeTransferReceived(address, uint256, bytes memory) external pure returns (bytes4) {
+ return NonPayableRecipient.fastBridgeTransferReceived.selector;
+ }
+}
diff --git a/packages/contracts-rfq/test/mocks/RecipientMock.sol b/packages/contracts-rfq/test/mocks/RecipientMock.sol
new file mode 100644
index 0000000000..a35d4ac5ec
--- /dev/null
+++ b/packages/contracts-rfq/test/mocks/RecipientMock.sol
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import {IFastBridgeRecipient} from "../../contracts/interfaces/IFastBridgeRecipient.sol";
+
+/// @notice Recipient mock for testing purposes. DO NOT USE IN PRODUCTION.
+contract RecipientMock is IFastBridgeRecipient {
+ /// @notice Mock needs to accept ETH
+ receive() external payable {}
+
+ /// @notice Minimal viable implementation of the fastBridgeTransferReceived hook.
+ function fastBridgeTransferReceived(address, uint256, bytes memory) external payable returns (bytes4) {
+ return IFastBridgeRecipient.fastBridgeTransferReceived.selector;
+ }
+}
diff --git a/packages/rfq-indexer/api/CHANGELOG.md b/packages/rfq-indexer/api/CHANGELOG.md
index 407b13c1ab..3a70fb5e6e 100644
--- a/packages/rfq-indexer/api/CHANGELOG.md
+++ b/packages/rfq-indexer/api/CHANGELOG.md
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [1.0.6](https://github.com/synapsecns/sanguine/compare/@synapsecns/rfq-indexer-api@1.0.5...@synapsecns/rfq-indexer-api@1.0.6) (2024-10-07)
+
+**Note:** Version bump only for package @synapsecns/rfq-indexer-api
+
+
+
+
+
## [1.0.5](https://github.com/synapsecns/sanguine/compare/@synapsecns/rfq-indexer-api@1.0.4...@synapsecns/rfq-indexer-api@1.0.5) (2024-10-03)
**Note:** Version bump only for package @synapsecns/rfq-indexer-api
diff --git a/packages/rfq-indexer/api/README.md b/packages/rfq-indexer/api/README.md
index 171802c523..8ab888a27d 100644
--- a/packages/rfq-indexer/api/README.md
+++ b/packages/rfq-indexer/api/README.md
@@ -6,34 +6,40 @@ To make requests, use: https://triumphant-magic-production.up.railway.app , and
## API Calls
-1. GET /api/hello
- - Description: A simple hello world endpoint
- - Example: `curl http://localhost:3001/api/hello`
-2. GET /api/pending-transactions-missing-relay
+All API calls can be viewed in Swagger:
+
+[Swagger Documentation](http://localhost:3001/api-docs)
+
+1. GET /api/pending-transactions-missing-relay
- Description: Retrieves pending transactions that are missing relay events
- Example:
```
- curl http://localhost:3001/api/pending-transactions-missing-relay
+ curl http://localhost:3001/api/pending-transactions/missing-relay
```
-3. GET /api/pending-transactions-missing-proof
+2. GET /api/pending-transactions-missing-proof
- Description: Retrieves pending transactions that are missing proof events
- Example:
```
- curl http://localhost:3001/api/pending-transactions-missing-proof
+ curl http://localhost:3001/api/pending-transactions/missing-proof
```
-4. GET /api/pending-transactions-missing-claim
+3. GET /api/pending-transactions-missing-claim
- Description: Retrieves pending transactions that are missing claim events
- Example:
```
- curl http://localhost:3001/api/pending-transactions-missing-claim
+ curl http://localhost:3001/api/pending-transactions/missing-claim
```
-5. GraphQL endpoint: /graphql
+4. GraphQL endpoint: /graphql
- Description: Provides a GraphQL interface for querying indexed data, the user is surfaced an interface to query the data via GraphiQL
+## Env Vars
+
+- **NODE_ENV**: Set to `"development"` for localhost testing.
+- **DATABASE_URL**: PostgreSQL connection URL for the ponder index.
+
## Important Scripts
- `yarn dev:local`: Runs the API in development mode using local environment variables
diff --git a/packages/rfq-indexer/api/package.json b/packages/rfq-indexer/api/package.json
index b38b6706a8..e69511c2b5 100644
--- a/packages/rfq-indexer/api/package.json
+++ b/packages/rfq-indexer/api/package.json
@@ -1,7 +1,7 @@
{
"name": "@synapsecns/rfq-indexer-api",
"private": true,
- "version": "1.0.5",
+ "version": "1.0.6",
"description": "",
"main": "index.js",
"scripts": {
diff --git a/packages/rfq-indexer/api/src/controllers/conflictingProofsController.ts b/packages/rfq-indexer/api/src/controllers/conflictingProofsController.ts
index 5079bb0d5f..7f1de3137a 100644
--- a/packages/rfq-indexer/api/src/controllers/conflictingProofsController.ts
+++ b/packages/rfq-indexer/api/src/controllers/conflictingProofsController.ts
@@ -13,7 +13,7 @@ export const conflictingProofsController = async (
const query = db
.with('deposits', () => qDeposits())
.with('relays', () => qRelays())
- .with('proofs', () => qProofs())
+ .with('proofs', () => qProofs({activeOnly: true}))
.with('combined', (qb) =>
qb
.selectFrom('deposits')
@@ -41,10 +41,10 @@ export const conflictingProofsController = async (
if (conflictingProofs && conflictingProofs.length > 0) {
res.json(conflictingProofs)
} else {
- res.status(200).json({ message: 'No conflicting proofs found' })
+ res.status(200).json({ message: 'No active conflicting proofs found' })
}
} catch (error) {
- console.error('Error fetching conflicting proofs:', error)
+ console.error('Error fetching active conflicting proofs:', error)
res.status(500).json({ message: 'Internal server error' })
}
}
diff --git a/packages/rfq-indexer/api/src/controllers/disputesController.ts b/packages/rfq-indexer/api/src/controllers/disputesController.ts
new file mode 100644
index 0000000000..2e65632c77
--- /dev/null
+++ b/packages/rfq-indexer/api/src/controllers/disputesController.ts
@@ -0,0 +1,27 @@
+import { Request, Response } from 'express'
+
+import { db } from '../db'
+import { qDisputes } from '../queries'
+import { nest_results } from '../utils/nestResults'
+
+export const disputesController = async (req: Request, res: Response) => {
+ try {
+ const query = db
+ .with('disputes', () => qDisputes({activeOnly: true}))
+ .selectFrom('disputes')
+ .selectAll()
+ .orderBy('blockTimestamp_dispute', 'desc')
+
+ const results = await query.execute()
+ const disputes = nest_results(results)
+
+ if (disputes && disputes.length > 0) {
+ res.json(disputes)
+ } else {
+ res.status(200).json({ message: 'No active disputes found' })
+ }
+ } catch (error) {
+ console.error('Error fetching active disputes:', error)
+ res.status(500).json({ message: 'Internal server error' })
+ }
+}
diff --git a/packages/rfq-indexer/api/src/controllers/pendingTransactionsController.ts b/packages/rfq-indexer/api/src/controllers/pendingTransactionsController.ts
index dfef98c11e..a2012c53c4 100644
--- a/packages/rfq-indexer/api/src/controllers/pendingTransactionsController.ts
+++ b/packages/rfq-indexer/api/src/controllers/pendingTransactionsController.ts
@@ -1,9 +1,18 @@
import { Request, Response } from 'express'
import { db } from '../db'
-import { qDeposits, qRelays, qProofs, qClaims, qRefunds } from '../queries'
+import {
+ qDeposits,
+ qRelays,
+ qProofs,
+ qClaims,
+ qRefunds,
+ qDisputes,
+} from '../queries'
import { nest_results } from '../utils/nestResults'
+const sevenDaysAgo = Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60
+
export const pendingTransactionsMissingClaimController = async (
req: Request,
res: Response
@@ -12,7 +21,7 @@ export const pendingTransactionsMissingClaimController = async (
const query = db
.with('deposits', () => qDeposits())
.with('relays', () => qRelays())
- .with('proofs', () => qProofs())
+ .with('proofs', () => qProofs({activeOnly: true}))
.with('claims', () => qClaims())
.with('combined', (qb) =>
qb
@@ -45,7 +54,6 @@ export const pendingTransactionsMissingClaimController = async (
}
}
-
export const pendingTransactionsMissingProofController = async (
req: Request,
res: Response
@@ -54,7 +62,7 @@ export const pendingTransactionsMissingProofController = async (
const query = db
.with('deposits', () => qDeposits())
.with('relays', () => qRelays())
- .with('proofs', () => qProofs())
+ .with('proofs', () => qProofs({activeOnly: true}))
.with('combined', (qb) =>
qb
.selectFrom('deposits')
@@ -111,6 +119,52 @@ export const pendingTransactionsMissingRelayController = async (
.selectFrom('combined')
.selectAll()
.orderBy('blockTimestamp_deposit', 'desc')
+ .where('blockTimestamp_deposit', '>', sevenDaysAgo)
+
+ const results = await query.execute()
+ const nestedResults = nest_results(results)
+
+ if (nestedResults && nestedResults.length > 0) {
+ res.json(nestedResults)
+ } else {
+ res
+ .status(404)
+ .json({ message: 'No pending transactions missing relay found' })
+ }
+ } catch (error) {
+ console.error('Error fetching pending transactions missing relay:', error)
+ res.status(500).json({ message: 'Internal server error' })
+ }
+}
+
+export const pendingTransactionsMissingRelayExceedDeadlineController = async (
+ req: Request,
+ res: Response
+) => {
+ try {
+ const query = db
+ .with('deposits', () => qDeposits())
+ .with('relays', () => qRelays())
+ .with('refunds', () => qRefunds())
+ .with(
+ 'combined',
+ (qb) =>
+ qb
+ .selectFrom('deposits')
+ .selectAll('deposits')
+ .leftJoin('relays', 'transactionId_deposit', 'transactionId_relay')
+ .leftJoin(
+ 'refunds',
+ 'transactionId_deposit',
+ 'transactionId_refund'
+ )
+ .where('transactionId_relay', 'is', null) // is not relayed
+ .where('transactionId_refund', 'is', null) // is not refunded
+ )
+ .selectFrom('combined')
+ .selectAll()
+ .orderBy('blockTimestamp_deposit', 'desc')
+ .where('blockTimestamp_deposit', '<=', sevenDaysAgo)
const results = await query.execute()
const nestedResults = nest_results(results)
diff --git a/packages/rfq-indexer/api/src/controllers/transactionIdController.ts b/packages/rfq-indexer/api/src/controllers/transactionIdController.ts
index 73857496fa..586590220a 100644
--- a/packages/rfq-indexer/api/src/controllers/transactionIdController.ts
+++ b/packages/rfq-indexer/api/src/controllers/transactionIdController.ts
@@ -1,7 +1,7 @@
import { Request, Response } from 'express'
import { db } from '../db'
-import { qDeposits, qRelays, qProofs, qClaims, qRefunds } from '../queries'
+import { qDeposits, qRelays, qProofs, qClaims, qRefunds, qDisputes } from '../queries'
import { nest_results } from '../utils/nestResults'
export const getTransactionById = async (req: Request, res: Response) => {
@@ -13,7 +13,8 @@ export const getTransactionById = async (req: Request, res: Response) => {
qDeposits().where('transactionId', '=', transactionId as string)
)
.with('relays', () => qRelays())
- .with('proofs', () => qProofs())
+ .with('proofs', () => qProofs({activeOnly: false})) // display proofs even if they have been invalidated/replaced by a dispute
+ .with('disputes', () => qDisputes({activeOnly: true})) // do not show disputes that have been invalidated/replaced by a proof
.with('claims', () => qClaims())
.with('refunds', () => qRefunds())
.with('combined', (qb) =>
@@ -21,11 +22,13 @@ export const getTransactionById = async (req: Request, res: Response) => {
.selectFrom('deposits')
.leftJoin('relays', 'transactionId_deposit', 'transactionId_relay')
.leftJoin('proofs', 'transactionId_deposit', 'transactionId_proof')
+ .leftJoin('disputes', 'transactionId_deposit', 'transactionId_dispute')
.leftJoin('claims', 'transactionId_deposit', 'transactionId_claim')
.leftJoin('refunds', 'transactionId_deposit', 'transactionId_refund')
.selectAll('deposits')
.selectAll('relays')
.selectAll('proofs')
+ .selectAll('disputes')
.selectAll('claims')
.selectAll('refunds')
)
diff --git a/packages/rfq-indexer/api/src/db/index.ts b/packages/rfq-indexer/api/src/db/index.ts
index 6e9826ad35..197eef0f60 100644
--- a/packages/rfq-indexer/api/src/db/index.ts
+++ b/packages/rfq-indexer/api/src/db/index.ts
@@ -7,6 +7,7 @@ import type {
BridgeProofProvidedEvents,
BridgeDepositRefundedEvents,
BridgeDepositClaimedEvents,
+ BridgeProofDisputedEvents,
} from '../types'
const { DATABASE_URL } = process.env
@@ -21,6 +22,7 @@ export interface Database {
BridgeProofProvidedEvents: BridgeProofProvidedEvents
BridgeDepositRefundedEvents: BridgeDepositRefundedEvents
BridgeDepositClaimedEvents: BridgeDepositClaimedEvents
+ BridgeProofDisputedEvents: BridgeProofDisputedEvents
}
export const db = new Kysely({ dialect })
diff --git a/packages/rfq-indexer/api/src/graphql/queries/queries.graphql b/packages/rfq-indexer/api/src/graphql/queries/queries.graphql
index 89398bb2ab..4dcfbf84eb 100644
--- a/packages/rfq-indexer/api/src/graphql/queries/queries.graphql
+++ b/packages/rfq-indexer/api/src/graphql/queries/queries.graphql
@@ -112,14 +112,21 @@ type ConflictingProof {
BridgeProof: Proof!
}
+type DisputedRelay {
+ Bridge: Transaction!
+ BridgeProof: Proof!
+}
+
type Query {
pendingTransactionsMissingRelay: [PendingTransactionMissingRelay!]!
+ pendingTransactionsMissingRelayExceedDeadline: [PendingTransactionMissingRelay!]!
pendingTransactionsMissingProof: [PendingTransactionMissingProof!]!
pendingTransactionsMissingClaim: [PendingTransactionMissingClaim!]!
transactionById(transactionId: String!): [CompleteTransaction!]!
recentInvalidRelays: [InvalidRelay!]!
refundedAndRelayedTransactions: [RefundedAndRelayedTransaction!]!
conflictingProofs: [ConflictingProof!]!
+ disputedRelays: [DisputedRelay!]!
}
diff --git a/packages/rfq-indexer/api/src/graphql/resolvers.ts b/packages/rfq-indexer/api/src/graphql/resolvers.ts
index 3ef04819ea..1deb7950d9 100644
--- a/packages/rfq-indexer/api/src/graphql/resolvers.ts
+++ b/packages/rfq-indexer/api/src/graphql/resolvers.ts
@@ -88,6 +88,20 @@ const qRefunds = () => {
])
}
+// typical fields to return for a BridgeProofDisputed event when it is joined to a BridgeRequest
+const qDisputes = () => {
+ return db
+ .selectFrom('BridgeProofDisputedEvents')
+ .select([
+ 'BridgeProofDisputedEvents.transactionId as transactionId_dispute',
+ 'BridgeProofDisputedEvents.blockNumber as blockNumber_dispute',
+ 'BridgeProofDisputedEvents.blockTimestamp as blockTimestamp_dispute',
+ 'BridgeProofDisputedEvents.transactionHash as transactionHash_dispute',
+ 'BridgeProofDisputedEvents.originChainId as originChainId_dispute',
+ 'BridgeProofDisputedEvents.originChain as originChain_dispute',
+ ])
+}
+
// using the suffix of a field, move it into a nested sub-object. This is a cleaner final resultset
// example: transactionHash_deposit:0xyz would get moved into BridgeRequest{transactionHash:0xyz}
//
@@ -220,6 +234,19 @@ const resolvers = {
'BridgeDepositClaimedEvents.originChain',
])
)
+ .unionAll(
+ db
+ .selectFrom('BridgeProofDisputedEvents')
+ .select([
+ 'BridgeProofDisputedEvents.id',
+ 'BridgeProofDisputedEvents.transactionId',
+ 'BridgeProofDisputedEvents.blockNumber',
+ 'BridgeProofDisputedEvents.blockTimestamp',
+ 'BridgeProofDisputedEvents.transactionHash',
+ 'BridgeProofDisputedEvents.originChainId',
+ 'BridgeProofDisputedEvents.originChain',
+ ])
+ )
if (filter) {
if (filter.transactionId) {
@@ -466,6 +493,29 @@ const resolvers = {
return nest_results(await query.execute())
},
+ disputedRelays: async () => {
+ const query = db
+ .with('deposits', () => qDeposits())
+ .with('relays', () => qRelays())
+ .with('proofs', () => qProofs())
+ .with('disputes', () => qDisputes())
+ .with('combined', (qb) =>
+ qb
+ .selectFrom('proofs')
+ .leftJoin(
+ 'disputes',
+ 'transactionId_proof',
+ 'transactionId_dispute'
+ )
+ .selectAll('proofs')
+ .selectAll('disputes')
+ )
+ .selectFrom('combined')
+ .selectAll()
+ .orderBy('blockTimestamp_proof', 'desc')
+
+ return nest_results(await query.execute())
+ },
},
BridgeEvent: {
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
diff --git a/packages/rfq-indexer/api/src/graphql/types/events.graphql b/packages/rfq-indexer/api/src/graphql/types/events.graphql
index 61f0f4cb4b..2e3716de0e 100644
--- a/packages/rfq-indexer/api/src/graphql/types/events.graphql
+++ b/packages/rfq-indexer/api/src/graphql/types/events.graphql
@@ -19,7 +19,7 @@ scalar BigInt
destChain: String!
sendChainGas: Boolean!
}
-
+
type BridgeRelayedEvent {
id: String!
transactionId: String!
@@ -39,7 +39,7 @@ scalar BigInt
destChainId: Int!
destChain: String!
}
-
+
type BridgeProofProvidedEvent {
id: String!
transactionId: String!
@@ -50,7 +50,7 @@ scalar BigInt
originChain: String!
relayer: String!
}
-
+
type BridgeDepositRefundedEvent {
id: String!
transactionId: String!
@@ -64,7 +64,7 @@ scalar BigInt
amount: BigInt!
amountFormatted: String!
}
-
+
type BridgeDepositClaimedEvent {
id: String!
transactionId: String!
@@ -78,4 +78,14 @@ scalar BigInt
token: String!
amount: BigInt!
amountFormatted: String!
- }
\ No newline at end of file
+}
+
+ type BridgeProofDisputedEvent {
+ id: String!
+ transactionId: String!
+ blockNumber: BigInt!
+ blockTimestamp: Int!
+ transactionHash: String!
+ originChainId: Int!
+ originChain: String!
+ }
diff --git a/packages/rfq-indexer/api/src/queries/disputesQueries.ts b/packages/rfq-indexer/api/src/queries/disputesQueries.ts
new file mode 100644
index 0000000000..829ba08a35
--- /dev/null
+++ b/packages/rfq-indexer/api/src/queries/disputesQueries.ts
@@ -0,0 +1,26 @@
+import { db } from '../db'
+
+export const qDisputes = ({ activeOnly }: { activeOnly: boolean } = { activeOnly: false}) => {
+ let query = db
+ .selectFrom('BridgeProofDisputedEvents')
+ .leftJoin('BridgeProofProvidedEvents', (join) =>
+ // if a proof occurred after this dispute, consider the dispute to be stale/invalid & ignore it
+ join
+ .onRef('BridgeProofProvidedEvents.transactionId', '=', 'BridgeProofDisputedEvents.transactionId')
+ .onRef('BridgeProofProvidedEvents.blockTimestamp', '>', 'BridgeProofDisputedEvents.blockTimestamp')
+ )
+ .select([
+ 'BridgeProofDisputedEvents.transactionId as transactionId_dispute',
+ 'BridgeProofDisputedEvents.blockNumber as blockNumber_dispute',
+ 'BridgeProofDisputedEvents.blockTimestamp as blockTimestamp_dispute',
+ 'BridgeProofDisputedEvents.transactionHash as transactionHash_dispute',
+ 'BridgeProofDisputedEvents.originChainId as originChainId_dispute',
+ 'BridgeProofDisputedEvents.originChain as originChain_dispute',
+ ]);
+
+ if (activeOnly) {
+ query = query.where('BridgeProofProvidedEvents.transactionId', 'is', null);
+ }
+
+ return query;
+}
diff --git a/packages/rfq-indexer/api/src/queries/index.ts b/packages/rfq-indexer/api/src/queries/index.ts
index 72bad2522e..da82fe8f87 100644
--- a/packages/rfq-indexer/api/src/queries/index.ts
+++ b/packages/rfq-indexer/api/src/queries/index.ts
@@ -1,5 +1,6 @@
export { qClaims } from './claimsQueries'
export { qDeposits } from './depositsQueries'
+export { qDisputes } from './disputesQueries'
export { qProofs } from './proofsQueries'
export { qRefunds } from './refundsQueries'
export { qRelays } from './relaysQueries'
diff --git a/packages/rfq-indexer/api/src/queries/proofsQueries.ts b/packages/rfq-indexer/api/src/queries/proofsQueries.ts
index e7a1ccc012..18a7b50325 100644
--- a/packages/rfq-indexer/api/src/queries/proofsQueries.ts
+++ b/packages/rfq-indexer/api/src/queries/proofsQueries.ts
@@ -1,15 +1,25 @@
import { db } from '../db'
// typical fields to return for a BridgeProofProvided event when it is joined to a BridgeRequest
-export const qProofs = () => {
- return db
+export const qProofs = ({ activeOnly}: { activeOnly: boolean} = { activeOnly: false}) => {
+ let query = db
.selectFrom('BridgeProofProvidedEvents')
+ .leftJoin('BridgeProofDisputedEvents', (join) =>
+ join
+ .onRef('BridgeProofDisputedEvents.transactionId', '=', 'BridgeProofProvidedEvents.transactionId')
+ .onRef('BridgeProofDisputedEvents.blockTimestamp', '>', 'BridgeProofProvidedEvents.blockTimestamp')
+ )
.select([
'BridgeProofProvidedEvents.transactionId as transactionId_proof',
'BridgeProofProvidedEvents.blockNumber as blockNumber_proof',
'BridgeProofProvidedEvents.blockTimestamp as blockTimestamp_proof',
'BridgeProofProvidedEvents.transactionHash as transactionHash_proof',
-
'BridgeProofProvidedEvents.relayer as relayer_proof',
- ])
-}
+ ]);
+
+ if (activeOnly) {
+ query = query.where('BridgeProofDisputedEvents.transactionId', 'is', null);
+ }
+
+ return query;
+}
\ No newline at end of file
diff --git a/packages/rfq-indexer/api/src/routes/disputesRoute.ts b/packages/rfq-indexer/api/src/routes/disputesRoute.ts
new file mode 100644
index 0000000000..de964220b0
--- /dev/null
+++ b/packages/rfq-indexer/api/src/routes/disputesRoute.ts
@@ -0,0 +1,65 @@
+import express from 'express'
+
+import { disputesController } from '../controllers/disputesController'
+
+const router = express.Router()
+
+/**
+ * @openapi
+ * /disputes:
+ * get:
+ * summary: Get all active disputes
+ * description: Retrieves a list of all active disputes
+ * responses:
+ * 200:
+ * description: Successful response
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * Bridge:
+ * type: object
+ * description: General transaction fields
+ * BridgeRequest:
+ * type: object
+ * description: Deposit information
+ * BridgeRelay:
+ * type: object
+ * description: Relay information
+ * BridgeRefund:
+ * type: object
+ * description: Refund information
+ * BridgeProof:
+ * type: object
+ * description: Proof information (if available)
+ * BridgeClaim:
+ * type: object
+ * description: Claim information (if available)
+ * BridgeDispute:
+ * type: object
+ * description: Dispute information (if available)
+ * 404:
+ * description: No disputes found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ * 500:
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ */
+router.get('/', disputesController)
+
+export default router
diff --git a/packages/rfq-indexer/api/src/routes/index.ts b/packages/rfq-indexer/api/src/routes/index.ts
index 302f878781..d85758c97b 100644
--- a/packages/rfq-indexer/api/src/routes/index.ts
+++ b/packages/rfq-indexer/api/src/routes/index.ts
@@ -5,6 +5,7 @@ import refundedAndRelayedRoute from './refundedAndRelayedRoute'
import invalidRelaysRoute from './invalidRelaysRoute'
import conflictingProofsRoute from './conflictingProofsRoute'
import transactionIdRoute from './transactionIdRoute'
+import disputesRoute from './disputesRoute'
const router = express.Router()
@@ -13,5 +14,5 @@ router.use('/refunded-and-relayed', refundedAndRelayedRoute)
router.use('/invalid-relays', invalidRelaysRoute)
router.use('/conflicting-proofs', conflictingProofsRoute)
router.use('/transaction-id', transactionIdRoute)
-
+router.use('/disputes', disputesRoute)
export default router
diff --git a/packages/rfq-indexer/api/src/routes/pendingTransactionsRoute.ts b/packages/rfq-indexer/api/src/routes/pendingTransactionsRoute.ts
index 2dbeafb121..e80dbd7fac 100644
--- a/packages/rfq-indexer/api/src/routes/pendingTransactionsRoute.ts
+++ b/packages/rfq-indexer/api/src/routes/pendingTransactionsRoute.ts
@@ -3,7 +3,8 @@ import express from 'express'
import {
pendingTransactionsMissingClaimController,
pendingTransactionsMissingProofController,
- pendingTransactionsMissingRelayController
+ pendingTransactionsMissingRelayController,
+ pendingTransactionsMissingRelayExceedDeadlineController
} from '../controllers/pendingTransactionsController'
const router = express.Router()
@@ -146,4 +147,46 @@ router.get('/missing-proof', pendingTransactionsMissingProofController)
*/
router.get('/missing-relay', pendingTransactionsMissingRelayController)
+/**
+ * @openapi
+ * /pending-transactions/exceed-deadline:
+ * get:
+ * summary: Get pending transactions exceed deadline
+ * description: Retrieves a list of transactions that have been deposited, but not yet relayed or refunded and have exceeded the deadline
+ * responses:
+ * 200:
+ * description: Successful response
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * deposit:
+ * type: object
+ * 404:
+ * description: No pending transactionst that exceed the deadline found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ * 500:
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ */
+router.get(
+ '/exceed-deadline',
+ pendingTransactionsMissingRelayExceedDeadlineController
+)
+
export default router
diff --git a/packages/rfq-indexer/api/src/types/index.ts b/packages/rfq-indexer/api/src/types/index.ts
index a536bead99..738e9469fd 100644
--- a/packages/rfq-indexer/api/src/types/index.ts
+++ b/packages/rfq-indexer/api/src/types/index.ts
@@ -81,6 +81,15 @@ export interface BridgeDepositClaimedEvents {
amountFormatted: ColumnType
}
+export interface BridgeProofDisputedEvents {
+ id: ColumnType
+ transactionId: ColumnType
+ blockNumber: ColumnType
+ blockTimestamp: ColumnType
+ transactionHash: ColumnType
+ chainId: ColumnType
+ chain: ColumnType
+}
// Add any other shared types used across the API
export type EventType =
| 'REQUEST'
@@ -88,7 +97,7 @@ export type EventType =
| 'PROOF_PROVIDED'
| 'DEPOSIT_REFUNDED'
| 'DEPOSIT_CLAIMED'
-
+ | 'DISPUTE'
export interface EventFilter {
type?: EventType
transactionId?: string