Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(contracts-rfq): relayer exclusivity [SLT-187] #3202

Merged
merged 36 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7109611
feat: scaffold exclusivity params
ChiTimesChi Sep 26, 2024
3125ba9
test: update to use V2 structs
ChiTimesChi Sep 26, 2024
2b3775e
test: exclusivity on DST chain
ChiTimesChi Sep 26, 2024
e06f942
test: exclusivity on SRC chain
ChiTimesChi Sep 26, 2024
c160a4e
feat: exclusivity params in bridge
ChiTimesChi Sep 26, 2024
0b71e5f
feat: update decoding in relay
ChiTimesChi Sep 26, 2024
b00b2e0
feat: check for exclusivity period in relay
ChiTimesChi Sep 26, 2024
0fdb834
test: disable parity tests (no longer backwards compatible)
ChiTimesChi Sep 26, 2024
72e0e7b
test: expect quoteID-related event
ChiTimesChi Sep 26, 2024
2f82a9a
feat: additional event for tracking quoteID
ChiTimesChi Sep 26, 2024
ff924d3
fix: ignore code-complexity error for now
ChiTimesChi Sep 26, 2024
1b26e0b
test: more coverage for `bridgeRelayDetails`
ChiTimesChi Sep 26, 2024
c8e9278
refactor: remove unnecessary casts in tests
ChiTimesChi Sep 26, 2024
98d4539
refactor: move txId check, comments
ChiTimesChi Sep 26, 2024
181410c
Merge branch 'master' into feat/FbV2-relayer-exclusivity
ChiTimesChi Sep 27, 2024
aaa5b2c
test: update for changes from #3204
ChiTimesChi Sep 27, 2024
a944c04
test: benchmark for SRC exclusivity
ChiTimesChi Sep 27, 2024
1582711
test: benchmark for DST exclusivity
ChiTimesChi Sep 27, 2024
023dd13
Merge branch 'master' into feat/FbV2-relayer-exclusivity
ChiTimesChi Sep 30, 2024
3601866
fix: decode into BridgeTransactionV2
ChiTimesChi Sep 30, 2024
be0cbaa
test: coverage for V1, V2 encoding
ChiTimesChi Sep 30, 2024
67f2b30
test: coverage for using V1 request instead of V2
ChiTimesChi Sep 30, 2024
784457f
refactor: unroll the nested v2 structure
ChiTimesChi Sep 30, 2024
dbdd0b8
test: update for the unrolled struct
ChiTimesChi Sep 30, 2024
047df43
refactor: make backwards-compatible view external
ChiTimesChi Sep 30, 2024
d31d882
chore: `foundryup` -> `forge fmt`
ChiTimesChi Sep 30, 2024
e130127
refactor: rename event
ChiTimesChi Sep 30, 2024
b6a6f57
Merge branch 'master' into feat/FbV2-relayer-exclusivity
ChiTimesChi Oct 1, 2024
023ed31
fix: post-merge getBridgeTransaction -> getBridgeTransactionV2
ChiTimesChi Oct 1, 2024
d6e76ca
refactor: move public `bridge()`, named vars
ChiTimesChi Oct 1, 2024
34378a5
test: use `quoteRelayer` as exclusivity flag
ChiTimesChi Oct 1, 2024
ef4aa2b
fix: always use `quoteExclusivitySeconds` as offset in `bridge()`
ChiTimesChi Oct 1, 2024
e5fce17
fix: don't check `exclusivityEndTime` on relays when `exclusivityRela…
ChiTimesChi Oct 1, 2024
415aa18
test: add cases for negative `quoteExclusivitySeconds`
ChiTimesChi Oct 1, 2024
6f5ffa7
test: enforce `0 < exclusivityEndTime <= deadline`
ChiTimesChi Oct 1, 2024
ad5bd73
feat: negative `quoteExclusivitySeconds`
ChiTimesChi Oct 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 65 additions & 32 deletions packages/contracts-rfq/contracts/FastBridgeV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -71,59 +71,87 @@

/// @inheritdoc IFastBridge
function getBridgeTransaction(bytes memory request) public pure returns (BridgeTransaction memory) {
// 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));
}

/// @inheritdoc IFastBridgeV2
function getBridgeTransactionV2(bytes memory request) public pure returns (BridgeTransactionV2 memory) {
return abi.decode(request, (BridgeTransactionV2));
}

/// @inheritdoc IFastBridge
function bridge(BridgeParams memory params) external payable {
BridgeParamsV2 memory defaultParamsV2 =
BridgeParamsV2({quoteRelayer: address(0), quoteExclusivitySeconds: 0, quoteId: bytes("")});
bridge(params, defaultParamsV2);
}

/// @inheritdoc IFastBridgeV2
// TODO: reduce cyclomatic complexity alongside arbitrary call
// solhint-disable-next-line code-complexity
function bridge(BridgeParams memory params, BridgeParamsV2 memory paramsV2) public payable {
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
// check bridge params
if (params.dstChainId == block.chainid) revert ChainIncorrect();
if (params.originAmount == 0 || params.destAmount == 0) revert AmountIncorrect();
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();
// quoteRelayer and quoteExclusivitySeconds must be both set or both not set
if ((paramsV2.quoteRelayer == address(0)) != (paramsV2.quoteExclusivitySeconds == 0)) {
revert ExclusivityParamsIncorrect();
}

// transfer tokens to bridge contract
// @dev use returned originAmount in request in case of transfer fees
uint256 originAmount = _pullToken(address(this), params.originToken, params.originAmount);

// track amount of origin token owed to protocol
uint256 originFeeAmount;

Check warning

Code scanning / Slither

Uninitialized local variables Medium

if (protocolFeeRate > 0) originFeeAmount = (originAmount * protocolFeeRate) / FEE_BPS;
originAmount -= originFeeAmount; // remove from amount used in request as not relevant for relayers

// Calculate exclusivity end time only if exclusivity params are set
uint256 exclusivityEndTime =
paramsV2.quoteExclusivitySeconds == 0 ? 0 : block.timestamp + paramsV2.quoteExclusivitySeconds;
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
// set status to requested
bytes memory request = abi.encode(
BridgeTransaction({
originChainId: uint32(block.chainid),
destChainId: params.dstChainId,
originSender: params.sender,
destRecipient: params.to,
originToken: params.originToken,
destToken: params.destToken,
originAmount: originAmount,
destAmount: params.destAmount,
originFeeAmount: originFeeAmount,
sendChainGas: params.sendChainGas,
deadline: params.deadline,
nonce: nonce++ // increment nonce on every bridge
BridgeTransactionV2({
txV1: BridgeTransaction({
originChainId: uint32(block.chainid),
destChainId: params.dstChainId,
originSender: params.sender,
destRecipient: params.to,
originToken: params.originToken,
destToken: params.destToken,
originAmount: originAmount,
destAmount: params.destAmount,
originFeeAmount: originFeeAmount,
sendChainGas: params.sendChainGas,
deadline: params.deadline,
nonce: nonce++ // increment nonce on every bridge
}),
exclusivityRelayer: paramsV2.quoteRelayer,
exclusivityEndTime: exclusivityEndTime
})
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
);
bytes32 transactionId = keccak256(request);
bridgeTxDetails[transactionId].status = BridgeStatus.REQUESTED;

emit BridgeRequested(
transactionId,
params.sender,
request,
params.dstChainId,
params.originToken,
params.destToken,
originAmount,
params.destAmount,
params.sendChainGas
);
emit BridgeRequestedV2(transactionId, paramsV2.quoteId);
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
}

/// @inheritdoc IFastBridge
function relay(bytes memory request) external payable {
Expand All @@ -131,28 +159,33 @@
}

/// @inheritdoc IFastBridgeV2
// TODO: reduce cyclomatic complexity alongside arbitrary call
// solhint-disable-next-line code-complexity
function relay(bytes memory request, address relayer) public payable {
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
if (relayer == address(0)) revert ZeroAddress();
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
// Check if the transaction has already been relayed
bytes32 transactionId = keccak256(request);
BridgeTransaction memory transaction = getBridgeTransaction(request);
if (transaction.destChainId != uint32(block.chainid)) revert ChainIncorrect();

// check haven't exceeded deadline for relay to happen
if (block.timestamp > transaction.deadline) revert DeadlineExceeded();

if (bridgeRelayDetails[transactionId].relayer != address(0)) revert TransactionRelayed();

if (bridgeRelays(transactionId)) revert TransactionRelayed();
// Decode the transaction and check that it could be relayed on this chain
BridgeTransactionV2 memory transaction = getBridgeTransactionV2(request);
if (transaction.txV1.destChainId != uint32(block.chainid)) revert ChainIncorrect();
// Check the deadline for relay to happen
if (block.timestamp > transaction.txV1.deadline) revert DeadlineExceeded();
// Check the exclusivity period, if it is still ongoing
if (block.timestamp <= transaction.exclusivityEndTime && relayer != transaction.exclusivityRelayer) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems like it might be possible for this to briefly delay a non-exclusive relay from occurring due to clock gaps btwn chains.

That is, if the dest chain is X seconds behind the origin, it would not be possible for anyone to relay until the EndTime is reached on the dest, no?

Should we restructure the checks like this:

if exclusivityRelayer is 0 or relayer, do not bother comparing exclusivity timestamp & just move along.

Otherwise, proceed to check that the exclusivity end time is in the past & revert if not.

This might also be more gas efficient overall

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For "non-exclusive txs" exclusivityEndTime == 0 (set by using paramsV2.quoteExclusivitySeconds = 0), so this will actually succeed without checking the relayer address.

However, the dest chain being behind would mean that the exclusivity period is extended longer than it should be for "exclusive txs". I guess we could treat this in two ways:

  1. Leave as is, and use much lower values on the FE for such cases (e.g. 1 second vs default value of 30 seconds).
  2. Allow negative values for quoteExclusivitySeconds so that the exclusivityEndTime could be set to a past timestamp origin-chain-wise, but a future timestamp for a destination chain, which is behind. In this casethe FE would use a negative value for such scenarios.

These are mostly similar, with the only difference being setting "exclusivity seconds" to either one second, or to a negative value (setting it to zero will turn off the exclusivity).

What do you think about these options?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I was neglecting this piece of bridge() and instead thinking that a non-excl would just have an endtime +0 seconds from deposit block ts
uint256 exclusivityEndTime = paramsV2.quoteExclusivitySeconds == 0 ? 0 : block.timestamp + paramsV2.quoteExclusivitySeconds;

option 2 seems like the better choice -- offers more flexibility & seems like it should not cost anything extra to allow a negative offset. If wanted to also allow zero exclusive seconds, could we switch to exclRelayer address = 0 to indicate/detect that exclusivity is off?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could go for quoteExclusivitySeconds being int256 rather than uint256 to allow negative values, don't see any concerns with that (txs having exclusivityEndTime in the past could be still completed by anyone).

As for allowing zero exclusive seconds with a quote relayer address, I think this is a very niche usecase, which might as well use +-1 instead of zero. Having this check in place allows to prevent bridge txs that mistakenly have only one exclusivity param from being initiated on the origin chain - this looks like a bigger concern to me than having to use +-1 instead of zero.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I take back the comment above. Having quoteRelayer as a single indicator of whether exclusivity is requested feels simple enough, and the incorrect integration (setting quote relayer but not seconds) isn't actually that bad - it's easily solvable and doesn't put anything at risk.

And this is also easier to enforce in the contracts. Will rework the tests and follow up with the contract changes

revert ExclusivityPeriodNotPassed();
}
// mark bridge transaction as relayed
bridgeRelayDetails[transactionId] =
BridgeRelay({blockNumber: uint48(block.number), blockTimestamp: uint48(block.timestamp), relayer: relayer});

// transfer tokens to recipient on destination chain and gas rebate if requested
address to = transaction.destRecipient;
address token = transaction.destToken;
uint256 amount = transaction.destAmount;
address to = transaction.txV1.destRecipient;
address token = transaction.txV1.destToken;
uint256 amount = transaction.txV1.destAmount;

uint256 rebate = chainGasAmount;
if (!transaction.sendChainGas) {
if (!transaction.txV1.sendChainGas) {
// forward erc20
rebate = 0;
_pullToken(to, token, amount);
Expand All @@ -169,11 +202,11 @@
transactionId,
relayer,
to,
transaction.originChainId,
transaction.originToken,
transaction.destToken,
transaction.originAmount,
transaction.destAmount,
transaction.txV1.originChainId,
transaction.txV1.originToken,
transaction.txV1.destToken,
transaction.txV1.originAmount,
transaction.txV1.destAmount,
rebate
);
}
Expand Down Expand Up @@ -229,7 +262,7 @@
/// @inheritdoc IFastBridge
function claim(bytes memory request, address to) public {
bytes32 transactionId = keccak256(request);
BridgeTransaction memory transaction = getBridgeTransaction(request);
BridgeTransaction memory transaction = getBridgeTransactionV2(request).txV1;

// update bridge tx status if able to claim origin collateral
if (bridgeTxDetails[transactionId].status != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect();
Expand Down Expand Up @@ -278,7 +311,7 @@
function refund(bytes memory request) external {
bytes32 transactionId = keccak256(request);

BridgeTransaction memory transaction = getBridgeTransaction(request);
BridgeTransaction memory transaction = getBridgeTransactionV2(request).txV1;

if (bridgeTxDetails[transactionId].status != BridgeStatus.REQUESTED) revert StatusIncorrect();

Expand Down
35 changes: 35 additions & 0 deletions packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,37 @@ interface IFastBridgeV2 is IFastBridge {
address relayer;
}

/// @notice New params introduced in the FastBridgeV2.
/// We are passing fields from the older BridgeParams struct outside of this struct
/// for backwards compatibility.
/// Note: quoteRelayer and quoteExclusivitySeconds are either both zero (indicating no exclusivity)
/// or both non-zero (indicating exclusivity for the given period).
/// @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
struct BridgeParamsV2 {
address quoteRelayer;
uint256 quoteExclusivitySeconds;
bytes quoteId;
}

/// @notice Updated bridge transaction struct to include parameters introduced in FastBridgeV2.
/// Note: only `exclusivityRelayer` can fill such a transaction until `exclusivityEndTime`.
/// TODO: consider changing the encoding scheme to prevent spending extra gas on decoding.
struct BridgeTransactionV2 {
BridgeTransaction txV1;
address exclusivityRelayer;
uint256 exclusivityEndTime;
}

event BridgeRequestedV2(bytes32 indexed transactionId, bytes quoteId);
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved

/// @notice Initiates bridge on origin chain to be relayed by off-chain relayer, with the ability
/// to provide temporary exclusivity fill rights for the quote relayer.
/// @param params The parameters required to bridge
/// @param paramsV2 The parameters for exclusivity fill rights (optional, could be left empty)
function bridge(BridgeParams memory params, BridgeParamsV2 memory paramsV2) external payable;

/// @notice Relays destination side of bridge transaction by off-chain relayer
/// @param request The encoded bridge transaction to relay on destination chain
/// @param relayer The address of the relaying entity which should have control of the origin funds when claimed
Expand Down Expand Up @@ -55,4 +86,8 @@ interface IFastBridgeV2 is IFastBridge {
/// @return timestamp The timestamp of the bridge proof
/// @return relayer The relayer address of the bridge proof
function bridgeProofs(bytes32 transactionId) external view returns (uint96 timestamp, address relayer);

/// @notice Decodes bridge request into a bridge transaction V2 struct used by FastBridgeV2
/// @param request The bridge request to decode
function getBridgeTransactionV2(bytes memory request) external view returns (BridgeTransactionV2 memory);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.0;
interface IFastBridgeV2Errors {
error AmountIncorrect();
error ChainIncorrect();
error ExclusivityParamsIncorrect();
error MsgValueIncorrect();
error SenderIncorrect();
error StatusIncorrect();
Expand All @@ -14,6 +15,7 @@ interface IFastBridgeV2Errors {
error DeadlineTooShort();
error DisputePeriodNotPassed();
error DisputePeriodPassed();
error ExclusivityPeriodNotPassed();

error TransactionRelayed();
}
8 changes: 4 additions & 4 deletions packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {FastBridgeV2, FastBridgeV2Test, IFastBridge} from "./FastBridgeV2.t.sol";
import {FastBridgeV2, FastBridgeV2Test, IFastBridgeV2} from "./FastBridgeV2.t.sol";
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved

// solhint-disable func-name-mixedcase, ordering
contract FastBridgeV2DstBaseTest is FastBridgeV2Test {
uint256 public constant LEFTOVER_BALANCE = 1 ether;

function setUp() public override {
function setUp() public virtual override {
vm.chainId(DST_CHAIN_ID);
super.setUp();
}
Expand All @@ -29,7 +29,7 @@ contract FastBridgeV2DstBaseTest is FastBridgeV2Test {

// ══════════════════════════════════════════════════ HELPERS ══════════════════════════════════════════════════════

function relay(address caller, uint256 msgValue, IFastBridge.BridgeTransaction memory bridgeTx) public {
function relay(address caller, uint256 msgValue, IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) public {
bytes memory request = abi.encode(bridgeTx);
vm.prank({msgSender: caller, txOrigin: caller});
fastBridge.relay{value: msgValue}(request);
Expand All @@ -39,7 +39,7 @@ contract FastBridgeV2DstBaseTest is FastBridgeV2Test {
address caller,
address relayer,
uint256 msgValue,
IFastBridge.BridgeTransaction memory bridgeTx
IFastBridgeV2.BridgeTransactionV2 memory bridgeTx
)
public
{
Expand Down
Loading
Loading