Skip to content

Commit

Permalink
feat(contracts-rfq): relayer exclusivity [SLT-187] (#3202)
Browse files Browse the repository at this point in the history
* feat: scaffold exclusivity params

* test: update to use V2 structs

* test: exclusivity on DST chain

* test: exclusivity on SRC chain

* feat: exclusivity params in bridge

* feat: update decoding in relay

* feat: check for exclusivity period in relay

* test: disable parity tests (no longer backwards compatible)

* test: expect quoteID-related event

* feat: additional event for tracking quoteID

* fix: ignore code-complexity error for now

* test: more coverage for `bridgeRelayDetails`

* refactor: remove unnecessary casts in tests

* refactor: move txId check, comments

* test: update for changes from #3204

* test: benchmark for SRC exclusivity

* test: benchmark for DST exclusivity

* fix: decode into BridgeTransactionV2
Note: technically this isn't necessary AS OF NOW, as
V2 struct new fields are ignored by the V1 decoding func.

* test: coverage for V1, V2 encoding

* test: coverage for using V1 request instead of V2

* refactor: unroll the nested v2 structure

* test: update for the unrolled struct

* refactor: make backwards-compatible view external

* chore: `foundryup` -> `forge fmt`
yes, this is slightly annoying :(

* refactor: rename event

* fix: post-merge getBridgeTransaction -> getBridgeTransactionV2

* refactor: move public `bridge()`, named vars

* test: use `quoteRelayer` as exclusivity flag

* fix: always use `quoteExclusivitySeconds` as offset in `bridge()`

* fix: don't check `exclusivityEndTime` on relays when `exclusivityRelayer` is not set

* test: add cases for negative `quoteExclusivitySeconds`

* test: enforce `0 < exclusivityEndTime <= deadline`

* feat: negative `quoteExclusivitySeconds`
  • Loading branch information
ChiTimesChi authored Oct 1, 2024
1 parent dfd6701 commit eb3db7c
Show file tree
Hide file tree
Showing 15 changed files with 736 additions and 155 deletions.
151 changes: 93 additions & 58 deletions packages/contracts-rfq/contracts/FastBridgeV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,53 +41,10 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {

/// @inheritdoc IFastBridge
function bridge(BridgeParams memory params) external payable {
// 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();

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

// 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
})
);
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
);
bridge({
params: params,
paramsV2: BridgeParamsV2({quoteRelayer: address(0), quoteExclusivitySeconds: 0, quoteId: bytes("")})
});
}

/// @inheritdoc IFastBridge
Expand Down Expand Up @@ -125,7 +82,7 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {
function refund(bytes memory request) external {
bytes32 transactionId = keccak256(request);

BridgeTransaction memory transaction = getBridgeTransaction(request);
BridgeTransactionV2 memory transaction = getBridgeTransactionV2(request);

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

Expand Down Expand Up @@ -156,18 +113,96 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {
return _timeSince(bridgeTxDetails[transactionId].proofBlockTimestamp) > DISPUTE_PERIOD;
}

/// @inheritdoc IFastBridge
function getBridgeTransaction(bytes memory request) external pure returns (BridgeTransaction memory) {
// Note: when passing V2 request, this will decode the V1 fields correctly since the new fields were
// 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
// TODO: reduce cyclomatic complexity alongside arbitrary call
// solhint-disable-next-line code-complexity
function bridge(BridgeParams memory params, BridgeParamsV2 memory paramsV2) public payable {
// 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();
int256 exclusivityEndTime = int256(block.timestamp) + paramsV2.quoteExclusivitySeconds;
// exclusivityEndTime must be in range (0 .. params.deadline]
if (exclusivityEndTime <= 0 || exclusivityEndTime > int256(params.deadline)) {
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;
if (protocolFeeRate > 0) originFeeAmount = (originAmount * protocolFeeRate) / FEE_BPS;
originAmount -= originFeeAmount; // remove from amount used in request as not relevant for relayers

// set status to requested
bytes memory request = abi.encode(
BridgeTransactionV2({
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,
// We checked exclusivityEndTime to be in range (0 .. params.deadline] above, so can safely cast
exclusivityEndTime: uint256(exclusivityEndTime)
})
);
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 BridgeQuoteDetails(transactionId, paramsV2.quoteId);
}

/// @inheritdoc IFastBridgeV2
// TODO: reduce cyclomatic complexity alongside arbitrary call
// solhint-disable-next-line code-complexity
function relay(bytes memory request, address relayer) public payable {
if (relayer == address(0)) revert ZeroAddress();
// Check if the transaction has already been relayed
bytes32 transactionId = keccak256(request);
BridgeTransaction memory transaction = getBridgeTransaction(request);
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.destChainId != uint32(block.chainid)) revert ChainIncorrect();

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

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

// Check the exclusivity period, if it is still ongoing
// forgefmt: disable-next-item
if (
transaction.exclusivityRelayer != address(0) &&
transaction.exclusivityRelayer != relayer &&
block.timestamp <= transaction.exclusivityEndTime
) {
revert ExclusivityPeriodNotPassed();
}
// mark bridge transaction as relayed
bridgeRelayDetails[transactionId] =
BridgeRelay({blockNumber: uint48(block.number), blockTimestamp: uint48(block.timestamp), relayer: relayer});
Expand Down Expand Up @@ -219,7 +254,7 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {
/// @inheritdoc IFastBridge
function claim(bytes memory request, address to) public {
bytes32 transactionId = keccak256(request);
BridgeTransaction memory transaction = getBridgeTransaction(request);
BridgeTransactionV2 memory transaction = getBridgeTransactionV2(request);

// update bridge tx status if able to claim origin collateral
if (bridgeTxDetails[transactionId].status != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect();
Expand Down Expand Up @@ -263,9 +298,9 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors {
return bridgeRelayDetails[transactionId].relayer != address(0);
}

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

/// @notice Pulls a requested token from the user to the requested recipient.
Expand Down
46 changes: 46 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,48 @@ 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;
int256 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 {
uint32 originChainId;
uint32 destChainId;
address originSender; // user (origin)
address destRecipient; // user (dest)
address originToken;
address destToken;
uint256 originAmount; // amount in on origin bridge less originFeeAmount
uint256 destAmount;
uint256 originFeeAmount;
bool sendChainGas;
uint256 deadline; // user specified deadline for destination relay
uint256 nonce;
address exclusivityRelayer;
uint256 exclusivityEndTime;
}

event BridgeQuoteDetails(bytes32 indexed transactionId, bytes quoteId);

/// @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 +97,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";

// 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

0 comments on commit eb3db7c

Please sign in to comment.