synapsecns/sanguine

View on GitHub
packages/contracts-rfq/contracts/FastBridge.sol

Summary

Maintainability
Test Coverage
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

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

import "./libs/Errors.sol";
import {UniversalTokenLib} from "./libs/UniversalToken.sol";

import {Admin} from "./Admin.sol";
import {IFastBridge} from "./interfaces/IFastBridge.sol";

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

contract FastBridge is IFastBridge, MulticallTarget, Admin {
    using SafeERC20 for IERC20;
    using UniversalTokenLib for address;

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

    enum BridgeStatus {
        NULL, // doesn't exist yet
        REQUESTED,
        RELAYER_PROVED,
        RELAYER_CLAIMED,
        REFUNDED
    }

    /// @notice Status of the bridge tx on origin chain
    mapping(bytes32 => BridgeStatus) public bridgeStatuses;
    /// @notice Proof of relayed bridge tx on origin chain
    mapping(bytes32 => BridgeProof) public bridgeProofs;
    /// @notice Whether bridge has been relayed on destination chain
    mapping(bytes32 => bool) public bridgeRelays;

    /// @dev to prevent replays
    uint256 public nonce;
    // @dev the block the contract was deployed at
    uint256 public immutable deployBlock;

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

    /// @notice Pulls a requested token from the user to the requested recipient.
    /// @dev Be careful of re-entrancy issues when msg.value > 0 and recipient != address(this)
    function _pullToken(address recipient, address token, uint256 amount) internal returns (uint256 amountPulled) {
        if (token != UniversalTokenLib.ETH_ADDRESS) {
            token.assertIsContract();
            // Record token balance before transfer
            amountPulled = IERC20(token).balanceOf(recipient);
            // Token needs to be pulled only if msg.value is zero
            // This way user can specify WETH as the origin asset
            IERC20(token).safeTransferFrom(msg.sender, recipient, amount);
            // Use the difference between the recorded balance and the current balance as the amountPulled
            amountPulled = IERC20(token).balanceOf(recipient) - amountPulled;
        } else {
            // Otherwise, we need to check that ETH amount matches msg.value
            if (amount != msg.value) revert MsgValueIncorrect();
            // Transfer value to recipient if not this address
            if (recipient != address(this)) token.universalTransfer(recipient, amount);
            // We will forward msg.value in the external call later, if recipient is not this contract
            amountPulled = msg.value;
        }
    }

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

    /// @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.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);
        bridgeStatuses[transactionId] = BridgeStatus.REQUESTED;

        emit BridgeRequested(
            transactionId,
            params.sender,
            request,
            params.dstChainId,
            params.originToken,
            params.destToken,
            originAmount,
            params.destAmount,
            params.sendChainGas
        );
    }

    /// @inheritdoc IFastBridge
    function relay(bytes memory request) external payable onlyRole(RELAYER_ROLE) {
        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();

        // mark bridge transaction as relayed
        if (bridgeRelays[transactionId]) revert TransactionRelayed();
        bridgeRelays[transactionId] = true;

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

        uint256 rebate = chainGasAmount;
        if (!transaction.sendChainGas) {
            // forward erc20
            rebate = 0;
            _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
            _pullToken(to, token, amount);
            _pullToken(to, UniversalTokenLib.ETH_ADDRESS, rebate);
        }

        emit BridgeRelayed(
            transactionId,
            msg.sender,
            to,
            transaction.originChainId,
            transaction.originToken,
            transaction.destToken,
            transaction.originAmount,
            transaction.destAmount,
            rebate
        );
    }

    /// @inheritdoc IFastBridge
    function prove(bytes memory request, bytes32 destTxHash) external onlyRole(RELAYER_ROLE) {
        bytes32 transactionId = keccak256(request);
        // update bridge tx status given proof provided
        if (bridgeStatuses[transactionId] != BridgeStatus.REQUESTED) revert StatusIncorrect();
        bridgeStatuses[transactionId] = BridgeStatus.RELAYER_PROVED;
        bridgeProofs[transactionId] = BridgeProof({timestamp: uint96(block.timestamp), relayer: msg.sender}); // overflow ok

        emit BridgeProofProvided(transactionId, msg.sender, destTxHash);
    }

    /// @notice Calculates time since proof submitted
    /// @dev proof.timestamp stores casted uint96(block.timestamp) block timestamps for gas optimization
    ///      _timeSince(proof) can accomodate rollover case when block.timestamp > type(uint96).max but
    ///      proof.timestamp < type(uint96).max via unchecked statement
    /// @param proof The bridge proof
    /// @return delta Time delta since proof submitted
    function _timeSince(BridgeProof memory proof) internal view returns (uint256 delta) {
        unchecked {
            delta = uint96(block.timestamp) - proof.timestamp;
        }
    }

    /// @inheritdoc IFastBridge
    function canClaim(bytes32 transactionId, address relayer) external view returns (bool) {
        if (bridgeStatuses[transactionId] != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect();
        BridgeProof memory proof = bridgeProofs[transactionId];
        if (proof.relayer != relayer) revert SenderIncorrect();
        return _timeSince(proof) > DISPUTE_PERIOD;
    }

    /// @inheritdoc IFastBridge
    function claim(bytes memory request, address to) external onlyRole(RELAYER_ROLE) {
        bytes32 transactionId = keccak256(request);
        BridgeTransaction memory transaction = getBridgeTransaction(request);

        // update bridge tx status if able to claim origin collateral
        if (bridgeStatuses[transactionId] != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect();

        BridgeProof memory proof = bridgeProofs[transactionId];
        if (proof.relayer != msg.sender) revert SenderIncorrect();
        if (_timeSince(proof) <= DISPUTE_PERIOD) revert DisputePeriodNotPassed();

        bridgeStatuses[transactionId] = BridgeStatus.RELAYER_CLAIMED;

        // update protocol fees if origin fee amount exists
        if (transaction.originFeeAmount > 0) protocolFees[transaction.originToken] += transaction.originFeeAmount;

        // transfer origin collateral less fee to specified address
        address token = transaction.originToken;
        uint256 amount = transaction.originAmount;
        token.universalTransfer(to, amount);

        emit BridgeDepositClaimed(transactionId, msg.sender, to, token, amount);
    }

    /// @inheritdoc IFastBridge
    function dispute(bytes32 transactionId) external onlyRole(GUARD_ROLE) {
        if (bridgeStatuses[transactionId] != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect();
        if (_timeSince(bridgeProofs[transactionId]) > DISPUTE_PERIOD) revert DisputePeriodPassed();

        // @dev relayer gets slashed effectively if dest relay has gone thru
        bridgeStatuses[transactionId] = BridgeStatus.REQUESTED;
        delete bridgeProofs[transactionId];

        emit BridgeProofDisputed(transactionId, msg.sender);
    }

    /// @inheritdoc IFastBridge
    function refund(bytes memory request) external {
        bytes32 transactionId = keccak256(request);
        BridgeTransaction memory transaction = getBridgeTransaction(request);

        if (hasRole(REFUNDER_ROLE, msg.sender)) {
            // Refunder can refund if deadline has passed
            if (block.timestamp <= transaction.deadline) revert DeadlineNotExceeded();
        } else {
            // Permissionless refund is allowed after REFUND_DELAY
            if (block.timestamp <= transaction.deadline + REFUND_DELAY) revert DeadlineNotExceeded();
        }

        // set status to refunded if still in requested state
        if (bridgeStatuses[transactionId] != BridgeStatus.REQUESTED) revert StatusIncorrect();
        bridgeStatuses[transactionId] = BridgeStatus.REFUNDED;

        // transfer origin collateral back to original sender
        address to = transaction.originSender;
        address token = transaction.originToken;
        uint256 amount = transaction.originAmount + transaction.originFeeAmount;
        token.universalTransfer(to, amount);

        emit BridgeDepositRefunded(transactionId, to, token, amount);
    }
}