Skip to content

Commit

Permalink
Feat: fast bridge router (#309)
Browse files Browse the repository at this point in the history
* Add interface with TODO notes

* Scaffold FastBridgeRouter

* Scaffold tests for FBRouter

* Setup tokens, pool

* Setup user

* Tests: bridge w/o origin swap

* Tests: bridge with origin swap

* Test for `getOriginAmountOut`

* Reduce amount of copypasta in the test

* Move common test stuff into abstract parent

* Tests: starting from ETH

* Tests: starting from WETH

* Tests: starting from token paired with WETH

* Tests: getOriginAmountOut for ETH/WETH and paired token

* Add tests for the revert scenarios

* Check both msg.value and data for expected FastBridge call

* First draft for `bridge()`

* Proper implementation of gas rebate flag

* Add configurable SwapQuoter

* Update tests to use SwapQuoter V1 and V2 for setup

* Implement `getOriginAmountOut()`

* Add docs, bridgeTokens -> rfqTokens

* Make FastBridge address configurable

* Script for FastBridgeRouter deployment

* Add FastBridge deployment artifacts

* Deploy FastBridgeRouter

* Update FastBridge deployment artifacts

* Deploy on Mainnet

* Update fast bridge address
  • Loading branch information
ChiTimesChi authored Feb 28, 2024
1 parent 895270c commit c6ebc2e
Showing 23 changed files with 4,655 additions and 1 deletion.
105 changes: 105 additions & 0 deletions contracts/rfq/FastBridgeRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {DefaultRouter} from "../router/DefaultRouter.sol";
import {UniversalTokenLib} from "../router/libs/UniversalToken.sol";
import {ActionLib, LimitedToken} from "../router/libs/Structs.sol";
import {IFastBridge} from "./interfaces/IFastBridge.sol";
import {IFastBridgeRouter, SwapQuery} from "./interfaces/IFastBridgeRouter.sol";
import {ISwapQuoter} from "./interfaces/ISwapQuoter.sol";

import {Ownable} from "@openzeppelin/contracts-4.5.0/access/Ownable.sol";

contract FastBridgeRouter is DefaultRouter, Ownable, IFastBridgeRouter {
using UniversalTokenLib for address;

/// @notice Emitted when the swap quoter is set.
/// @param newSwapQuoter The new swap quoter.
event SwapQuoterSet(address newSwapQuoter);

/// @notice Emitted when the new FastBridge contract is set.
/// @param newFastBridge The new FastBridge contract.
event FastBridgeSet(address newFastBridge);

/// @inheritdoc IFastBridgeRouter
bytes1 public constant GAS_REBATE_FLAG = 0x2A;

/// @inheritdoc IFastBridgeRouter
address public fastBridge;
/// @inheritdoc IFastBridgeRouter
address public swapQuoter;

constructor(address owner_) {
transferOwnership(owner_);
}

/// @inheritdoc IFastBridgeRouter
function setFastBridge(address fastBridge_) external onlyOwner {
fastBridge = fastBridge_;
emit FastBridgeSet(fastBridge_);
}

/// @inheritdoc IFastBridgeRouter
function setSwapQuoter(address swapQuoter_) external onlyOwner {
swapQuoter = swapQuoter_;
emit SwapQuoterSet(swapQuoter_);
}

/// @inheritdoc IFastBridgeRouter
function bridge(
address recipient,
uint256 chainId,
address token,
uint256 amount,
SwapQuery memory originQuery,
SwapQuery memory destQuery
) external payable {
if (originQuery.hasAdapter()) {
// Perform a swap using the swap adapter, set this contract as recipient
(token, amount) = _doSwap(address(this), token, amount, originQuery);
} else {
// Otherwise, pull the token from the user to this contract
amount = _pullToken(address(this), token, amount);
}
IFastBridge.BridgeParams memory params = IFastBridge.BridgeParams({
dstChainId: uint32(chainId),
sender: msg.sender,
to: recipient,
originToken: token,
destToken: destQuery.tokenOut,
originAmount: amount,
destAmount: destQuery.minAmountOut,
sendChainGas: _chainGasRequested(destQuery.rawParams),
deadline: destQuery.deadline
});
token.universalApproveInfinity(fastBridge, amount);
uint256 msgValue = token == UniversalTokenLib.ETH_ADDRESS ? amount : 0;
IFastBridge(fastBridge).bridge{value: msgValue}(params);
}

/// @inheritdoc IFastBridgeRouter
function getOriginAmountOut(
address tokenIn,
address[] memory rfqTokens,
uint256 amountIn
) external view returns (SwapQuery[] memory originQueries) {
uint256 len = rfqTokens.length;
originQueries = new SwapQuery[](len);
for (uint256 i = 0; i < len; ++i) {
originQueries[i] = ISwapQuoter(swapQuoter).getAmountOut(
LimitedToken({actionMask: ActionLib.allActions(), token: tokenIn}),
rfqTokens[i],
amountIn
);
// Adjust the Adapter address if it exists
if (originQueries[i].hasAdapter()) {
originQueries[i].routerAdapter = address(this);
}
}
}

/// @dev Checks if the explicit instruction to send gas to the destination chain was provided.
function _chainGasRequested(bytes memory rawParams) internal pure returns (bool) {
return rawParams.length > 0 && rawParams[0] == GAS_REBATE_FLAG;
}
}
93 changes: 93 additions & 0 deletions contracts/rfq/interfaces/IFastBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// TODO: This should be pulled from the sanguine repo (requires publish and relaxing the ^0.8.20 pragma)
interface IFastBridge {
struct BridgeTransaction {
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;
uint256 nonce;
}

struct BridgeProof {
uint96 timestamp;
address relayer;
}

// ============ Events ============

event BridgeRequested(bytes32 transactionId, address sender, bytes request);
event BridgeRelayed(
bytes32 transactionId,
address relayer,
address to,
address token,
uint256 amount,
uint256 chainGasAmount
);
event BridgeProofProvided(bytes32 transactionId, address relayer, bytes32 transactionHash);
event BridgeProofDisputed(bytes32 transactionId, address relayer);
event BridgeDepositClaimed(bytes32 transactionId, address relayer, address to, address token, uint256 amount);
event BridgeDepositRefunded(bytes32 transactionId, address to, address token, uint256 amount);

// ============ Methods ============

struct BridgeParams {
uint32 dstChainId;
address sender;
address to;
address originToken;
address destToken;
uint256 originAmount; // should include protocol fee (if any)
uint256 destAmount; // should include relayer fee
bool sendChainGas;
uint256 deadline;
}

/// @notice Initiates bridge on origin chain to be relayed by off-chain relayer
/// @param params The parameters required to bridge
function bridge(BridgeParams memory params) external payable;

/// @notice Relays destination side of bridge transaction by off-chain relayer
/// @param request The encoded bridge transaction to relay on destination chain
function relay(bytes memory request) external payable;

/// @notice Provides proof on origin side that relayer provided funds on destination side of bridge transaction
/// @param request The encoded bridge transaction to prove on origin chain
/// @param destTxHash The destination tx hash proving bridge transaction was relayed
function prove(bytes memory request, bytes32 destTxHash) external;

/// @notice Completes bridge transaction on origin chain by claiming originally deposited capital
/// @param request The encoded bridge transaction to claim on origin chain
/// @param to The recipient address of the funds
function claim(bytes memory request, address to) external;

/// @notice Disputes an outstanding proof in case relayer provided dest chain tx is invalid
/// @param transactionId The transaction id associated with the encoded bridge transaction to dispute
function dispute(bytes32 transactionId) external;

/// @notice Refunds an outstanding bridge transaction in case optimistic bridging failed
/// @param request The encoded bridge transaction to refund
/// @param to The recipient address of the funds
function refund(bytes memory request, address to) external;

// ============ Views ============

/// @notice Decodes bridge request into a bridge transaction
/// @param request The bridge request to decode
function getBridgeTransaction(bytes memory request) external pure returns (BridgeTransaction memory);

/// @notice Checks if the dispute period has passed so bridge deposit can be claimed
/// @param transactionId The transaction id associated with the encoded bridge transaction to check
/// @param relayer The address of the relayer attempting to claim
function canClaim(bytes32 transactionId, address relayer) external view returns (bool);
}
78 changes: 78 additions & 0 deletions contracts/rfq/interfaces/IFastBridgeRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {SwapQuery} from "../../router/libs/Structs.sol";

interface IFastBridgeRouter {
/// @notice Sets the address of the FastBridge contract
/// @dev This function is only callable by the owner
/// @param fastBridge_ The address of the FastBridge contract
function setFastBridge(address fastBridge_) external;

/// @notice Sets the address of the SwapQuoter contract
/// @dev This function is only callable by the owner
/// @param swapQuoter_ The address of the SwapQuoter contract
function setSwapQuoter(address swapQuoter_) external;

/// @notice Initiate an RFQ transaction with an optional swap on origin chain,
/// and an optional gas rebate on destination chain.
/// @dev Note that method is payable.
/// If token is ETH_ADDRESS, this method should be invoked with `msg.value = amountIn`.
/// If token is ERC20, the tokens will be pulled from msg.sender (use `msg.value = 0`).
/// Make sure to approve this contract for spending `token` beforehand.
///
/// `originQuery` is supposed to be fetched using FastBridgeRouter.getOriginAmountOut().
/// Alternatively one could use an external adapter for more complex swaps on the origin chain.
/// `destQuery.rawParams` signals whether the user wants to receive a gas rebate on the destination chain:
/// - If the first byte of `destQuery.rawParams` is GAS_REBATE_FLAG, the user wants to receive a gas rebate.
/// - Otherwise, the user does not want to receive a gas rebate.
///
/// Cross-chain RFQ swap will be performed between tokens: `originQuery.tokenOut` and `destQuery.tokenOut`.
/// Note: both tokens could be ETH_ADDRESS or ERC20.
/// Full proceeds of the origin swap are considered the bid for the cross-chain swap.
/// `destQuery.minAmountOut` is considered the ask for the cross-chain swap.
/// Note: applying slippage to `destQuery.minAmountOut` will result in a worse price for the user,
/// the full Relayer quote should be used instead.
/// @param recipient Address to receive tokens on destination chain
/// @param chainId Destination chain id
/// @param token Initial token to be pulled from the user
/// @param amount Amount of the initial tokens to be pulled from the user
/// @param originQuery Origin swap query (see above)
/// @param destQuery Destination swap query (see above)
function bridge(
address recipient,
uint256 chainId,
address token,
uint256 amount,
SwapQuery memory originQuery,
SwapQuery memory destQuery
) external payable;

// ═══════════════════════════════════════════════════ VIEWS ═══════════════════════════════════════════════════════

/// @notice Finds the best path between `tokenIn` and every RFQ token from the given list,
/// treating the swap as "origin swap", without putting any restrictions on the swap.
/// @dev Check (query.minAmountOut != 0): this is true only if the swap is possible.
/// The returned queries with minAmountOut != 0 could be used as `originQuery` with FastBridgeRouter.
/// Note: it is possible to form a SwapQuery off-chain using alternative SwapAdapter for the origin swap.
/// @param tokenIn Initial token that user wants to bridge/swap
/// @param rfqTokens List of RFQ tokens
/// @param amountIn Amount of tokens user wants to bridge/swap
/// @return originQueries List of structs that could be used as `originQuery` in FastBridgeRouter.
/// minAmountOut and deadline fields will need to be adjusted based on the user settings.
function getOriginAmountOut(
address tokenIn,
address[] memory rfqTokens,
uint256 amountIn
) external view returns (SwapQuery[] memory originQueries);

/// @notice Magic value that indicates that the user wants to receive gas rebate on the destination chain.
/// This is the answer to the ultimate question of life, the universe, and everything.
function GAS_REBATE_FLAG() external view returns (bytes1);

/// @notice Address of the FastBridge contract, used to initiate cross-chain RFQ swaps.
function fastBridge() external view returns (address);

/// @notice Address of the SwapQuoter contract, used to fetch quotes for the origin swap.
function swapQuoter() external view returns (address);
}
12 changes: 12 additions & 0 deletions contracts/rfq/interfaces/ISwapQuoter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {LimitedToken, SwapQuery} from "../../router/libs/Structs.sol";

interface ISwapQuoter {
function getAmountOut(
LimitedToken memory tokenIn,
address tokenOut,
uint256 amountIn
) external view returns (SwapQuery memory query);
}
Loading

0 comments on commit c6ebc2e

Please sign in to comment.