Skip to main content
PulseScanner.io

Address

0x2ef5eafa8711e2441bd519eed5d09f8dfef2ecf3
Current Holdings
$0.00
TXs sent
not counted
First Active
2026-05-16
block 26,542,292
Last Active
124 days ago
block 26,542,292
Funded By
not identified

Net worth historyi

No net-worth snapshots recorded yet
exact matchBridgeRoutersolc 0.8.19+commit.7dd6d404runtime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol";
import { ReentrancyGuard } from "solmate/utils/ReentrancyGuard.sol";
import {
    IBridgeAmountAdapter
} from "contracts/interfaces/IBridgeAmountAdapter.sol";
import { IRouterAllowlist } from "contracts/interfaces/IRouterAllowlist.sol";
import { SwapRouter } from "contracts/SwapRouter.sol";
import { SwapStep } from "contracts/dex/SwapTypes.sol";

/// @title BridgeRouter
/// @notice Executes source-chain bridge calls, optionally after an MSR swap.
/// @dev Keeps bridge entrypoints out of `SwapRouter`. Source swaps are
/// composed through the deployed swap-only MSR instead of inheriting its full
/// public ABI or embedding its DEX execution code.
contract BridgeRouter is ReentrancyGuard {
    uint256 public constant BPS_DENOMINATOR = 10_000;

    IRouterAllowlist public immutable allowlist;
    SwapRouter public swapRouter;

    address public admin;

    error BridgeFailed(bytes error);
    error BridgeDidNotConsumeTokens();
    error InsufficientEthInput();
    error InsufficientOutputAmount(uint256 received, uint256 minRequired);
    error Expired();
    error InvalidAddress();
    error InvalidStepsLength();
    error InvalidTokenPath();
    error NotAdmin();

    /// @dev `feeAmount` is reported by `SwapRouter.swapWithFee` on the
    /// swap output.
    event SwapAndBridge(
        address indexed sender,
        address indexed bridgeToken,
        address indexed bridgeAdapter,
        uint256 amountIn,
        uint256 bridgeAmount,
        uint256 feeAmount
    );

    /// @dev `feeAmount` is charged by BridgeRouter using the current MSR fee
    /// bps.
    event Bridge(
        address indexed sender,
        address indexed bridgeToken,
        address indexed bridgeAdapter,
        uint256 amountIn,
        uint256 bridgeAmount,
        uint256 feeAmount
    );

    event Sweep(address indexed token, uint256 amount);
    event AdminUpdated(address oldAdmin, address newAdmin);
    event SwapRouterUpdated(
        address oldSwapRouter, address newSwapRouter
    );

    struct SwapBridgeEventData {
        address sender;
        address bridgeToken;
        address bridgeAdapter;
        uint256 amountIn;
    }

    struct BridgeContext {
        address bridgeAdapter;
        address bridgeToken;
        bytes bridgeParams;
        address recipient;
        uint256 bridgeAmount;
        uint256 bridgeNativeValue;
        uint256 initialBridgeTokenBalance;
        uint256 preCallEthBalance;
    }

    constructor(
        address allowlist_,
        address swapRouter_,
        address admin_
    ) {
        if (allowlist_ == address(0)) revert InvalidAddress();
        if (swapRouter_ == address(0)) revert InvalidAddress();
        if (admin_ == address(0)) revert InvalidAddress();

        allowlist = IRouterAllowlist(allowlist_);
        _validateSwapRouter(swapRouter_);
        swapRouter = SwapRouter(payable(swapRouter_));
        admin = admin_;
    }

    /// @notice Execute a source swap followed by an exact-amount bridge
    /// adapter.
    /// @dev The adapter receives the actual post-fee bridgeToken amount at
    /// runtime. Native bridge tokens require a swap step that emits native
    /// ETH, such as UnwrapWETH. Use `bridge()` when the user already holds the
    /// bridgeable token.
    function swapAndBridge(
        SwapStep[] calldata steps,
        uint256 amountIn,
        uint256 minSwapOut,
        address bridgeAdapter,
        bytes calldata bridgeParams,
        address bridgeToken,
        address recipient,
        uint256 deadline
    ) external payable nonReentrant {
        if (block.timestamp > deadline) revert Expired();
        if (steps.length == 0) revert InvalidStepsLength();
        _validateBridgeAdapter(bridgeAdapter);

        _swapAndBridge(
            msg.sender,
            steps,
            amountIn,
            minSwapOut,
            bridgeAdapter,
            bridgeParams,
            bridgeToken,
            recipient,
            deadline
        );
    }

    function _swapAndBridge(
        address sender,
        SwapStep[] calldata steps,
        uint256 amountIn,
        uint256 minSwapOut,
        address bridgeAdapter,
        bytes calldata bridgeParams,
        address bridgeToken,
        address recipient,
        uint256 deadline
    ) internal {
        SwapBridgeEventData memory eventData = SwapBridgeEventData({
            sender: sender,
            bridgeToken: bridgeToken,
            bridgeAdapter: bridgeAdapter,
            amountIn: amountIn
        });

        uint256 preCallEthBalance = address(this).balance - msg.value;
        (uint256 bridgeNativeValue, uint256 fee, uint256 initialBalance) = _prepareBridgeAmount(
            steps, amountIn, minSwapOut, bridgeToken, deadline
        );

        BridgeContext memory bridgeContext = BridgeContext({
            bridgeAdapter: bridgeAdapter,
            bridgeToken: bridgeToken,
            bridgeParams: bridgeParams,
            recipient: recipient,
            bridgeAmount: _balanceOf(bridgeToken) - initialBalance,
            bridgeNativeValue: bridgeNativeValue,
            initialBridgeTokenBalance: initialBalance,
            preCallEthBalance: preCallEthBalance
        });
        if (bridgeContext.bridgeAmount < minSwapOut) {
            revert InsufficientOutputAmount(
                bridgeContext.bridgeAmount, minSwapOut
            );
        }

        uint256 consumed = _executeBridgeAndRefund(bridgeContext);

        _emitSwapAndBridge(eventData, consumed, fee);
    }

    /// @notice Bridge tokens cross-chain through an exact-amount adapter
    /// without any source swap.
    function bridge(
        address bridgeAdapter,
        bytes calldata bridgeParams,
        address bridgeToken,
        uint256 amountIn,
        address recipient,
        uint256 deadline
    ) external payable nonReentrant {
        if (block.timestamp > deadline) revert Expired();
        _validateBridgeAdapter(bridgeAdapter);

        uint256 preCallEthBalance = address(this).balance - msg.value;
        (uint256 bridgeNativeValue, uint256 fee, uint256 initialBalance) =
            _pullDirectBridgeAmount(bridgeToken, amountIn, preCallEthBalance);

        uint256 consumed = _executeBridgeAndRefund(
            BridgeContext({
                bridgeAdapter: bridgeAdapter,
                bridgeToken: bridgeToken,
                bridgeParams: bridgeParams,
                recipient: recipient,
                bridgeAmount: _balanceOf(bridgeToken) - initialBalance,
                bridgeNativeValue: bridgeNativeValue,
                initialBridgeTokenBalance: initialBalance,
                preCallEthBalance: preCallEthBalance
            })
        );

        emit Bridge(
            msg.sender, bridgeToken, bridgeAdapter, amountIn, consumed, fee
        );
    }

    /// @notice Sweep stuck ERC20 tokens to MSR's fee collector.
    function sweep(
        address token
    ) external {
        if (msg.sender != admin) revert NotAdmin();
        uint256 balance = IERC20(token).balanceOf(address(this));
        if (balance > 0) {
            // Bridge sweeps use the current MSR fee collector; changing MSR
            // also changes the future sweep destination.
            SafeTransferLib.safeTransfer(
                token, swapRouter.feeCollector(), balance
            );
            emit Sweep(token, balance);
        }
    }

    /// @notice Sweep stuck ETH to MSR's fee collector.
    function sweepETH() external {
        if (msg.sender != admin) revert NotAdmin();
        uint256 balance = address(this).balance;
        if (balance > 0) {
            // Bridge sweeps use the current MSR fee collector; changing MSR
            // also changes the future sweep destination.
            SafeTransferLib.safeTransferETH(
                swapRouter.feeCollector(), balance
            );
            emit Sweep(address(0), balance);
        }
    }

    /// @notice Set a new admin.
    function setAdmin(
        address newAdmin
    ) external {
        if (msg.sender != admin) revert NotAdmin();
        if (newAdmin == address(0)) revert InvalidAddress();
        emit AdminUpdated(admin, newAdmin);
        admin = newAdmin;
    }

    /// @notice Point source swaps at a newly deployed swap-only MSR.
    /// @dev Admin can redirect all future `swapAndBridge` source-swap
    /// execution; treat this with the same trust model as other governed
    /// router updates.
    function setSwapRouter(
        address newSwapRouter
    ) external {
        if (msg.sender != admin) revert NotAdmin();
        _validateSwapRouter(newSwapRouter);
        address oldSwapRouter = address(swapRouter);
        if (newSwapRouter == oldSwapRouter) return;
        swapRouter = SwapRouter(payable(newSwapRouter));
        emit SwapRouterUpdated(oldSwapRouter, newSwapRouter);
    }

    function _prepareBridgeAmount(
        SwapStep[] calldata steps,
        uint256 amountIn,
        uint256 minSwapOut,
        address bridgeToken,
        uint256 deadline
    )
        internal
        returns (uint256 bridgeNativeValue, uint256 fee, uint256 initialBalance)
    {
        // Caller guarantees at least one swap step.
        if (steps[steps.length - 1].tokenOut != bridgeToken) {
            revert InvalidTokenPath();
        }

        bool isNativeInput = steps[0].tokenIn == address(0);
        if (isNativeInput && msg.value < amountIn) {
            revert InsufficientEthInput();
        }

        initialBalance = _balanceOf(bridgeToken);
        if (isNativeInput && bridgeToken == address(0)) {
            initialBalance -= amountIn;
        }
        (bridgeNativeValue, fee) =
            _executeSourceSwap(steps, amountIn, minSwapOut, deadline);
    }

    function _executeSourceSwap(
        SwapStep[] calldata steps,
        uint256 amountIn,
        uint256 minSwapOut,
        uint256 deadline
    ) internal returns (uint256 bridgeNativeValue, uint256 fee) {
        address tokenIn = steps[0].tokenIn;
        // Snapshot the MSR pointer so one bridge flow cannot observe two
        // different MSR deployments.
        SwapRouter currentMsr = swapRouter;
        if (tokenIn == address(0)) {
            bridgeNativeValue = msg.value - amountIn;
            (, fee) = currentMsr.swapWithFee{ value: amountIn }(
                steps, amountIn, minSwapOut, address(this), deadline
            );
        } else {
            bridgeNativeValue = msg.value;
            uint256 balanceBefore = IERC20(tokenIn).balanceOf(address(this));
            SafeTransferLib.safeTransferFrom(
                tokenIn, msg.sender, address(this), amountIn
            );
            uint256 actualInput =
                IERC20(tokenIn).balanceOf(address(this)) - balanceBefore;

            _approve(tokenIn, address(currentMsr), actualInput);
            (, fee) = currentMsr.swapWithFee(
                steps, actualInput, minSwapOut, address(this), deadline
            );
            _revokeApproval(tokenIn, address(currentMsr));
        }
    }

    function _emitSwapAndBridge(
        SwapBridgeEventData memory eventData,
        uint256 consumed,
        uint256 fee
    ) private {
        emit SwapAndBridge(
            eventData.sender,
            eventData.bridgeToken,
            eventData.bridgeAdapter,
            eventData.amountIn,
            consumed,
            fee
        );
    }

    function _pullDirectBridgeAmount(
        address bridgeToken,
        uint256 amountIn,
        uint256 preCallEthBalance
    )
        internal
        returns (uint256 bridgeNativeValue, uint256 fee, uint256 initialBalance)
    {
        bool isNativeBridge = bridgeToken == address(0);
        SwapRouter currentMsr = swapRouter;
        uint256 feeBps = currentMsr.feeBps();
        address feeCollector = currentMsr.feeCollector();

        if (isNativeBridge) {
            if (msg.value < amountIn) revert InsufficientEthInput();
            bridgeNativeValue = msg.value - amountIn;
            initialBalance = preCallEthBalance + bridgeNativeValue;
            // Native input has no fee-on-transfer behavior to net out.
            fee = (amountIn * feeBps) / BPS_DENOMINATOR;
            if (fee > 0) {
                SafeTransferLib.safeTransferETH(feeCollector, fee);
            }
        } else {
            bridgeNativeValue = msg.value;
            initialBalance = IERC20(bridgeToken).balanceOf(address(this));

            SafeTransferLib.safeTransferFrom(
                bridgeToken, msg.sender, address(this), amountIn
            );

            uint256 amountReceived =
                IERC20(bridgeToken).balanceOf(address(this)) - initialBalance;
            fee = (amountReceived * feeBps) / BPS_DENOMINATOR;
            if (fee > 0) {
                SafeTransferLib.safeTransfer(bridgeToken, feeCollector, fee);
            }
        }
    }

    function _executeBridgeAndRefund(
        BridgeContext memory context
    ) internal returns (uint256 consumed) {
        bool isNativeBridge = context.bridgeToken == address(0);
        uint256 balanceBeforeBridge = _balanceOf(context.bridgeToken);

        if (!isNativeBridge) {
            _approve(
                context.bridgeToken, context.bridgeAdapter, context.bridgeAmount
            );
        }

        {
            uint256 callValue = isNativeBridge
                ? context.bridgeNativeValue + context.bridgeAmount
                : context.bridgeNativeValue;
            bytes memory adapterCalldata = abi.encodeCall(
                IBridgeAmountAdapter.bridgeExactAmount,
                (
                    context.bridgeToken,
                    context.bridgeAmount,
                    context.bridgeParams
                )
            );
            (bool success, bytes memory result) =
                context.bridgeAdapter.call{ value: callValue }(adapterCalldata);
            if (!success) revert BridgeFailed(result);
        }

        uint256 balanceAfterBridge = _balanceOf(context.bridgeToken);

        // `>=` catches both over-refund and exact-refund/no-consumption paths
        // before the balance-delta subtraction below.
        if (balanceAfterBridge >= balanceBeforeBridge) {
            revert BridgeDidNotConsumeTokens();
        }
        uint256 consumedBalance = balanceBeforeBridge - balanceAfterBridge;
        if (isNativeBridge) {
            // The bridge call also forwards `bridgeNativeValue` as the
            // relay fee. Require the adapter to consume the full
            // `bridgeAmount + bridgeNativeValue` so a retained relay fee
            // cannot mask under-consumption of the principal — adapters
            // that refund any portion of the relay fee must be priced
            // exactly by the caller.
            if (
                consumedBalance < context.bridgeAmount + context.bridgeNativeValue
            ) {
                revert BridgeDidNotConsumeTokens();
            }
            consumed = context.bridgeAmount;
        } else {
            consumed = consumedBalance;
        }
        if (consumed == 0) {
            revert BridgeDidNotConsumeTokens();
        }

        if (!isNativeBridge) {
            _revokeApproval(context.bridgeToken, context.bridgeAdapter);

            if (balanceAfterBridge > context.initialBridgeTokenBalance) {
                SafeTransferLib.safeTransfer(
                    context.bridgeToken,
                    context.recipient,
                    balanceAfterBridge - context.initialBridgeTokenBalance
                );
            }
        }

        uint256 ethBalance = address(this).balance;
        if (ethBalance > context.preCallEthBalance) {
            SafeTransferLib.safeTransferETH(
                context.recipient, ethBalance - context.preCallEthBalance
            );
        }
    }

    function _validateBridgeAdapter(
        address bridgeAdapter
    ) internal view {
        allowlist.requireAllowedBridge(bridgeAdapter);
        allowlist.requireAllowedSelector(
            bridgeAdapter, IBridgeAmountAdapter.bridgeExactAmount.selector
        );
    }

    function _validateSwapRouter(
        address router
    ) internal view {
        if (router == address(0)) revert InvalidAddress();
        // Hygiene only: admin must still choose a trusted MSR implementation.
        if (SwapRouter(payable(router)).feeCollector() == address(this)) {
            revert InvalidAddress();
        }
    }

    function _balanceOf(
        address token
    ) internal view returns (uint256) {
        if (token == address(0)) {
            return address(this).balance;
        }
        return IERC20(token).balanceOf(address(this));
    }

    function _approve(
        address token,
        address spender,
        uint256 amount
    ) internal {
        SafeTransferLib.safeApprove(token, spender, 0);
        SafeTransferLib.safeApprove(token, spender, amount);
    }

    function _revokeApproval(
        address token,
        address spender
    ) internal {
        SafeTransferLib.safeApprove(token, spender, 0);
    }

    receive() external payable { }
}