Skip to content

Commit

Permalink
Merge branch 'master' into feat/relayer-arb-call
Browse files Browse the repository at this point in the history
  • Loading branch information
dwasse committed Nov 18, 2024
2 parents 83cef1d + aec6dce commit 14ed93d
Show file tree
Hide file tree
Showing 43 changed files with 1,139 additions and 258 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# ignore ifacemaker files
*_generated.go linguist-generated
contrib/opbot/generated/* linguist-generated
*.contractinfo.json linguist-generated=true


# svg should be treated as a binary https://git.io/JE2VK
Expand Down
11 changes: 11 additions & 0 deletions packages/contracts-rfq/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.13.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/[email protected]...@synapsecns/[email protected]) (2024-11-18)


### Features

* **contracts-rfq:** rework permisionless cancellation [SLT-489] ([#3382](https://github.com/synapsecns/sanguine/issues/3382)) ([7932f41](https://github.com/synapsecns/sanguine/commit/7932f416341a227db295c33f232efde89fd2c50c))





## [0.12.1](https://github.com/synapsecns/sanguine/compare/@synapsecns/[email protected]...@synapsecns/[email protected]) (2024-11-11)

**Note:** Version bump only for package @synapsecns/contracts-rfq
Expand Down
104 changes: 104 additions & 0 deletions packages/contracts-rfq/contracts/AdminV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IAdminV2} from "./interfaces/IAdminV2.sol";
import {IAdminV2Errors} from "./interfaces/IAdminV2Errors.sol";

import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol";
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";

contract AdminV2 is AccessControlEnumerable, IAdminV2, IAdminV2Errors {
using SafeERC20 for IERC20;

/// @notice Address reserved for native gas token (ETH on Ethereum and most L2s, AVAX on Avalanche, etc)
address public constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

/// @notice Role identifier for Quoter API's off-chain authentication.
/// @dev Only addresses with this role can post FastBridge quotes to the API.
bytes32 public constant QUOTER_ROLE = keccak256("QUOTER_ROLE");

/// @notice Role identifier for Prover's on-chain authentication in FastBridge.
/// @dev Only addresses with this role can provide proofs that a FastBridge request has been relayed.
bytes32 public constant PROVER_ROLE = keccak256("PROVER_ROLE");

/// @notice Role identifier for Guard's on-chain authentication in FastBridge.
/// @dev Only addresses with this role can dispute submitted relay proofs during the dispute period.
bytes32 public constant GUARD_ROLE = keccak256("GUARD_ROLE");

/// @notice Role identifier for Canceler's on-chain authentication in FastBridge.
/// @dev Only addresses with this role can cancel a FastBridge transaction without the cancel delay.
bytes32 public constant CANCELER_ROLE = keccak256("CANCELER_ROLE");

/// @notice Role identifier for Governor's on-chain administrative authority.
/// @dev Only addresses with this role can perform administrative tasks within the contract.
bytes32 public constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE");

/// @notice Denominator for fee rates, represents 100%.
uint256 public constant FEE_BPS = 1e6;
/// @notice Maximum protocol fee rate: 1% on origin amount.
uint256 public constant FEE_RATE_MAX = 0.01e6;

/// @notice Minimum cancel delay that can be set by the governor.
uint256 public constant MIN_CANCEL_DELAY = 1 hours;
/// @notice Default cancel delay set during the contract deployment.
uint256 public constant DEFAULT_CANCEL_DELAY = 1 days;

/// @notice Protocol fee rate taken on origin amount deposited in origin chain
uint256 public protocolFeeRate;

/// @notice Protocol fee amounts accumulated
mapping(address => uint256) public protocolFees;

/// @notice Delay for a transaction after which it could be permisionlessly cancelled
uint256 public cancelDelay;

/// @notice This is deprecated and should not be used.
/// @dev Use ZapNative V2 requests instead.
uint256 public immutable chainGasAmount = 0;

constructor(address _owner) {
_grantRole(DEFAULT_ADMIN_ROLE, _owner);
_setCancelDelay(DEFAULT_CANCEL_DELAY);
}

/// @notice Allows the contract governor to set the cancel delay. The cancel delay is the time after the transaction
/// deadline after which it can be permissionlessly cancelled, if it hasn't been proven by any of the Relayers.
function setCancelDelay(uint256 newCancelDelay) external onlyRole(GOVERNOR_ROLE) {
_setCancelDelay(newCancelDelay);
}

/// @notice Allows the contract governor to set the protocol fee rate. The protocol fee is taken from the origin
/// amount only for completed and claimed transactions.
/// @dev The protocol fee is abstracted away from the relayers, they always operate using the amounts after fees:
/// what they see as the origin amount emitted in the log is what they get credited with.
function setProtocolFeeRate(uint256 newFeeRate) external onlyRole(GOVERNOR_ROLE) {
if (newFeeRate > FEE_RATE_MAX) revert FeeRateAboveMax();
uint256 oldFeeRate = protocolFeeRate;
protocolFeeRate = newFeeRate;
emit FeeRateUpdated(oldFeeRate, newFeeRate);
}

/// @notice Allows the contract governor to sweep the accumulated protocol fees in the contract.
function sweepProtocolFees(address token, address recipient) external onlyRole(GOVERNOR_ROLE) {
uint256 feeAmount = protocolFees[token];
if (feeAmount == 0) return; // skip if no accumulated fees

protocolFees[token] = 0;
emit FeesSwept(token, recipient, feeAmount);
/// Sweep the fees as the last transaction action
if (token == NATIVE_GAS_TOKEN) {
Address.sendValue(payable(recipient), feeAmount);
} else {
IERC20(token).safeTransfer(recipient, feeAmount);
}
}

/// @notice Internal function to set the cancel delay. Security checks are performed outside of this function.
function _setCancelDelay(uint256 newCancelDelay) private {
if (newCancelDelay < MIN_CANCEL_DELAY) revert CancelDelayBelowMin();
uint256 oldCancelDelay = cancelDelay;
cancelDelay = newCancelDelay;
emit CancelDelayUpdated(oldCancelDelay, newCancelDelay);
}
}
80 changes: 40 additions & 40 deletions packages/contracts-rfq/contracts/FastBridgeV2.sol
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
// SPDX-License-Identifier: MIT
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 {BridgeTransactionV2Lib} from "./libs/BridgeTransactionV2.sol";

import {Admin} from "./Admin.sol";
import {AdminV2} from "./AdminV2.sol";
import {IFastBridge} from "./interfaces/IFastBridge.sol";
import {IFastBridgeV2} from "./interfaces/IFastBridgeV2.sol";
import {IFastBridgeV2Errors} from "./interfaces/IFastBridgeV2Errors.sol";
import {IZapRecipient} from "./interfaces/IZapRecipient.sol";

import {MulticallTarget} from "./utils/MulticallTarget.sol";

import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";

/// @notice FastBridgeV2 is a contract for bridging tokens across chains.
contract FastBridgeV2 is Admin, MulticallTarget, IFastBridgeV2, IFastBridgeV2Errors {
contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2Errors {
using BridgeTransactionV2Lib for bytes;
using SafeERC20 for IERC20;

/// @notice Address reserved for native gas token (ETH on Ethereum and most L2s, AVAX on Avalanche, etc)
address public constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

/// @notice Dispute period for relayed transactions
uint256 public constant DISPUTE_PERIOD = 30 minutes;

/// @notice Delay for a transaction after which it could be permisionlessly refunded
uint256 public constant REFUND_DELAY = 7 days;

/// @notice Minimum deadline period to relay a requested bridge transaction
uint256 public constant MIN_DEADLINE_PERIOD = 30 minutes;

Expand All @@ -47,7 +41,7 @@ contract FastBridgeV2 is Admin, MulticallTarget, IFastBridgeV2, IFastBridgeV2Err
/// @notice the block the contract was deployed at
uint256 public immutable deployBlock;

constructor(address _owner) Admin(_owner) {
constructor(address _owner) AdminV2(_owner) {
deployBlock = block.number;
}

Expand Down Expand Up @@ -104,33 +98,10 @@ contract FastBridgeV2 is Admin, MulticallTarget, IFastBridgeV2, IFastBridgeV2Err
emit BridgeProofDisputed(transactionId, disputedRelayer);
}

/// Note: this function is deprecated and will be removed in a future version.
/// @inheritdoc IFastBridge
function refund(bytes calldata request) external {
request.validateV2();
bytes32 transactionId = keccak256(request);
BridgeTxDetails storage $ = bridgeTxDetails[transactionId];
// Can only refund a REQUESTED transaction after its deadline expires
if ($.status != BridgeStatus.REQUESTED) revert StatusIncorrect();
uint256 deadline = request.deadline();
// Permissionless refund is only allowed after REFUND_DELAY on top of the deadline
if (!hasRole(REFUNDER_ROLE, msg.sender)) deadline += REFUND_DELAY;
if (block.timestamp <= deadline) revert DeadlineNotExceeded();
// Update status to REFUNDED and return the full amount (collateral + protocol fees) to the original sender.
// The protocol fees are only updated when the transaction is claimed, so we don't need to update them here.
// Note: this is a storage write
$.status = BridgeStatus.REFUNDED;

address to = request.originSender();
address token = request.originToken();
uint256 amount = request.originAmount() + request.originFeeAmount();
// Emit the event before any external calls
emit BridgeDepositRefunded(transactionId, to, token, amount);
// Complete the user refund as the last transaction action
if (token == NATIVE_GAS_TOKEN) {
Address.sendValue(payable(to), amount);
} else {
IERC20(token).safeTransfer(to, amount);
}
cancel(request);
}

/// @inheritdoc IFastBridge
Expand Down Expand Up @@ -306,7 +277,7 @@ contract FastBridgeV2 is Admin, MulticallTarget, IFastBridgeV2, IFastBridgeV2Err
}

/// @inheritdoc IFastBridgeV2
function prove(bytes32 transactionId, bytes32 destTxHash, address relayer) public onlyRole(RELAYER_ROLE) {
function prove(bytes32 transactionId, bytes32 destTxHash, address relayer) public onlyRole(PROVER_ROLE) {
BridgeTxDetails storage $ = bridgeTxDetails[transactionId];

// Can only prove a REQUESTED transaction
Expand Down Expand Up @@ -363,6 +334,35 @@ contract FastBridgeV2 is Admin, MulticallTarget, IFastBridgeV2, IFastBridgeV2Err
}
}

/// @inheritdoc IFastBridgeV2
function cancel(bytes calldata request) public {
request.validateV2();
bytes32 transactionId = keccak256(request);
BridgeTxDetails storage $ = bridgeTxDetails[transactionId];
// Can only cancel a REQUESTED transaction after its deadline expires
if ($.status != BridgeStatus.REQUESTED) revert StatusIncorrect();
uint256 deadline = request.deadline();
// Permissionless cancel is only allowed after `cancelDelay` on top of the deadline
if (!hasRole(CANCELER_ROLE, msg.sender)) deadline += cancelDelay;
if (block.timestamp <= deadline) revert DeadlineNotExceeded();
// Update status to REFUNDED and return the full amount (collateral + protocol fees) to the original sender.
// The protocol fees are only updated when the transaction is claimed, so we don't need to update them here.
// Note: this is a storage write
$.status = BridgeStatus.REFUNDED;

address to = request.originSender();
address token = request.originToken();
uint256 amount = request.originAmount() + request.originFeeAmount();
// Emit the event before any external calls
emit BridgeDepositRefunded(transactionId, to, token, amount);
// Complete the user cancel as the last transaction action
if (token == NATIVE_GAS_TOKEN) {
Address.sendValue(payable(to), amount);
} else {
IERC20(token).safeTransfer(to, amount);
}
}

/// @inheritdoc IFastBridgeV2
function bridgeStatuses(bytes32 transactionId) public view returns (BridgeStatus status) {
return bridgeTxDetails[transactionId].status;
Expand All @@ -383,8 +383,8 @@ contract FastBridgeV2 is Admin, MulticallTarget, IFastBridgeV2, IFastBridgeV2Err
}

/// @notice Takes the bridged asset from the user into FastBridgeV2 custody. It will be later
/// claimed by the relayer who completed the relay on destination chain, or refunded back to the user,
/// should no one complete the relay.
/// claimed by the relayer who completed the relay on destination chain, or transferred back to the user
/// via the cancel function should no one complete the relay.
function _takeBridgedUserAsset(address token, uint256 amount) internal returns (uint256 amountTaken) {
if (token == NATIVE_GAS_TOKEN) {
// For the native gas token, we just need to check that the supplied msg.value is correct.
Expand Down
14 changes: 14 additions & 0 deletions packages/contracts-rfq/contracts/interfaces/IAdminV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

interface IAdminV2 {
event CancelDelayUpdated(uint256 oldCancelDelay, uint256 newCancelDelay);
event FeeRateUpdated(uint256 oldFeeRate, uint256 newFeeRate);
event FeesSwept(address token, address recipient, uint256 amount);

function setCancelDelay(uint256 newCancelDelay) external;

function setProtocolFeeRate(uint256 newFeeRate) external;

function sweepProtocolFees(address token, address recipient) external;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

interface IAdminV2Errors {
error CancelDelayBelowMin();
error FeeRateAboveMax();
}
6 changes: 6 additions & 0 deletions packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ interface IFastBridgeV2 is IFastBridge {
/// @notice Can only send funds to the relayer address on the proof.
/// @param request The encoded bridge transaction to claim on origin chain
function claim(bytes memory request) external;

/// @notice Cancels an outstanding bridge transaction in case optimistic bridging failed and returns the full amount
/// to the original sender.
/// @param request The encoded bridge transaction to refund
function cancel(bytes memory request) external;

/// @notice Checks if a transaction has been relayed
/// @param transactionId The ID of the transaction to check
/// @return True if the transaction has been relayed, false otherwise
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts-rfq/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@synapsecns/contracts-rfq",
"license": "MIT",
"version": "0.12.1",
"version": "0.13.0",
"description": "FastBridge contracts.",
"private": true,
"files": [
Expand Down
20 changes: 10 additions & 10 deletions packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -152,19 +152,19 @@ contract FastBridgeV2GasBenchmarkSrcTest is FastBridgeV2SrcBaseTest {
assertEq(srcToken.balanceOf(address(fastBridge)), initialFastBridgeBalanceToken);
}

function test_refundPermissioned_token() public {
function test_cancelPermissioned_token() public {
bytes32 txId = getTxId(bridgedTokenTx);
skipTimeAtLeast({time: DEADLINE});
refund({caller: refunder, bridgeTx: bridgedTokenTx});
cancel({caller: canceler, bridgeTx: bridgedTokenTx});
assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REFUNDED);
assertEq(srcToken.balanceOf(userA), initialUserBalanceToken + tokenParams.originAmount);
assertEq(srcToken.balanceOf(address(fastBridge)), initialFastBridgeBalanceToken - tokenParams.originAmount);
}

function test_refundPermissionless_token() public {
function test_cancelPermissionless_token() public {
bytes32 txId = getTxId(bridgedTokenTx);
skipTimeAtLeast({time: DEADLINE + PERMISSIONLESS_REFUND_DELAY});
refund({caller: userB, bridgeTx: bridgedTokenTx});
skipTimeAtLeast({time: DEADLINE + PERMISSIONLESS_CANCEL_DELAY});
cancel({caller: userB, bridgeTx: bridgedTokenTx});
assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REFUNDED);
assertEq(srcToken.balanceOf(userA), initialUserBalanceToken + tokenParams.originAmount);
assertEq(srcToken.balanceOf(address(fastBridge)), initialFastBridgeBalanceToken - tokenParams.originAmount);
Expand Down Expand Up @@ -236,19 +236,19 @@ contract FastBridgeV2GasBenchmarkSrcTest is FastBridgeV2SrcBaseTest {
assertEq(address(fastBridge).balance, initialFastBridgeBalanceEth);
}

function test_refundPermissioned_eth() public {
function test_cancelPermissioned_eth() public {
bytes32 txId = getTxId(bridgedEthTx);
skipTimeAtLeast({time: DEADLINE});
refund({caller: refunder, bridgeTx: bridgedEthTx});
cancel({caller: canceler, bridgeTx: bridgedEthTx});
assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REFUNDED);
assertEq(userA.balance, initialUserBalanceEth + ethParams.originAmount);
assertEq(address(fastBridge).balance, initialFastBridgeBalanceEth - ethParams.originAmount);
}

function test_refundPermissionless_eth() public {
function test_cancelPermissionless_eth() public {
bytes32 txId = getTxId(bridgedEthTx);
skipTimeAtLeast({time: DEADLINE + PERMISSIONLESS_REFUND_DELAY});
refund({caller: userB, bridgeTx: bridgedEthTx});
skipTimeAtLeast({time: DEADLINE + PERMISSIONLESS_CANCEL_DELAY});
cancel({caller: userB, bridgeTx: bridgedEthTx});
assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REFUNDED);
assertEq(userA.balance, initialUserBalanceEth + ethParams.originAmount);
assertEq(address(fastBridge).balance, initialFastBridgeBalanceEth - ethParams.originAmount);
Expand Down
Loading

0 comments on commit 14ed93d

Please sign in to comment.