BridgeLock

0x9145FC4fc5e99770c649Cf58e7713422FCffDDFcVerified
Native balance0USDX
Account typeSmart contract
NetworkTuven 99359

Overview

Contract name
BridgeLock
Verification
Verified source
Balance updated at block
13039

Verified contract source

Verified
Contract name
BridgeLock
Compiler
v0.8.29+commit.ab55807c
EVM version
prague
License
none
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./BridgeBase.sol";

/// @title BridgeLock
/// @notice Bridge lock contract deployed on the source chain (e.g. BSC).
///         Users lock tokens here; the relayer unlocks them when bridging back.
contract BridgeLock is BridgeBase, ReentrancyGuard {
    using SafeERC20 for IERC20;

    uint256 public nonce;

    /// @notice Tracks which nonces have been consumed (for unlock replay protection).
    mapping(uint256 => bool) public processedNonces;

    /// @notice Fees collected per token (address(0) = native), withdrawable to `feeRecipient`.
    ///         Kept separate from user deposits so an unlock can never spend fees or vice versa.
    mapping(address => uint256) public collectedFees;

    event FeesWithdrawn(address indexed token, address indexed to, uint256 amount);

    event Locked(
        address indexed token,
        address indexed from,
        uint256 amount,
        uint256 destChainId,
        uint256 nonce
    );

    event Unlocked(
        address indexed token,
        address indexed to,
        uint256 amount,
        uint256 nonce
    );

    /// @param _relayer Address of the off-chain relayer that processes bridge messages
    constructor(address _relayer) BridgeBase(_relayer) {}

    /// @notice Lock ERC20 tokens to bridge them to the destination chain. The bridge
    ///         fee is taken here; `Locked.amount` is the NET amount the relayer mints.
    /// @param token       ERC20 token address
    /// @param amount      Amount of tokens to lock (gross, fee included)
    /// @param destChainId Destination chain ID
    function lock(address token, uint256 amount, uint256 destChainId) external whenNotPaused {
        require(amount > 0, "BridgeLock: zero amount");
        // Measure what actually arrived so fee-on-transfer tokens cannot over-credit.
        uint256 before = IERC20(token).balanceOf(address(this));
        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
        uint256 received = IERC20(token).balanceOf(address(this)) - before;
        require(received > 0, "BridgeLock: nothing received");

        (uint256 fee, uint256 net) = _splitFee(received);
        require(net > 0, "BridgeLock: amount too small");
        collectedFees[token] += fee;

        uint256 currentNonce = nonce;
        nonce++;

        if (fee > 0) emit FeeCollected(token, msg.sender, fee);
        emit Locked(token, msg.sender, net, destChainId, currentNonce);
    }

    /// @notice Lock native tokens to bridge them to the destination chain. The bridge
    ///         fee is taken here; `Locked.amount` is the NET amount the relayer mints.
    /// @param destChainId Destination chain ID
    function lockNative(uint256 destChainId) external payable whenNotPaused {
        require(msg.value > 0, "BridgeLock: zero amount");

        (uint256 fee, uint256 net) = _splitFee(msg.value);
        require(net > 0, "BridgeLock: amount too small");
        collectedFees[address(0)] += fee;

        uint256 currentNonce = nonce;
        nonce++;

        if (fee > 0) emit FeeCollected(address(0), msg.sender, fee);
        emit Locked(address(0), msg.sender, net, destChainId, currentNonce);
    }

    /// @notice Send the fees collected for `token` (address(0) = native) to `feeRecipient`.
    ///         Callable by anyone; the destination is fixed by the owner.
    function withdrawFees(address token) external nonReentrant returns (uint256 amount) {
        amount = collectedFees[token];
        require(amount > 0, "BridgeLock: no fees");
        collectedFees[token] = 0;
        address to = feeRecipient;

        if (token == address(0)) {
            (bool success, ) = to.call{value: amount}("");
            require(success, "BridgeLock: native transfer failed");
        } else {
            IERC20(token).safeTransfer(to, amount);
        }

        emit FeesWithdrawn(token, to, amount);
    }

    /// @notice Unlock tokens back to a user (called by relayer when bridging back).
    /// @param token  ERC20 token address (address(0) for native)
    /// @param to     Recipient address
    /// @param amount Amount to unlock
    /// @param _nonce Unique nonce from the burn event on the destination chain
    function unlock(address token, address to, uint256 amount, uint256 _nonce)
        external
        onlyRelayer
        nonReentrant
        whenNotPaused
    {
        require(!processedNonces[_nonce], "BridgeLock: nonce already processed");
        require(amount > 0, "BridgeLock: zero amount");
        require(to != address(0), "BridgeLock: zero recipient");

        processedNonces[_nonce] = true;

        if (token == address(0)) {
            (bool success, ) = to.call{value: amount}("");
            require(success, "BridgeLock: native transfer failed");
        } else {
            IERC20(token).safeTransfer(to, amount);
        }

        emit Unlocked(token, to, amount, _nonce);
    }

    /// @notice Allow the contract to receive native tokens (for unlocking native).
    receive() external payable {}
}
Contract ABI
JSON
[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_relayer",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "EnforcedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExpectedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "FeeCollected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldRecipient",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "FeeRecipientUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "oldFeeBps",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newFeeBps",
        "type": "uint256"
      }
    ],
    "name": "FeeUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FeesWithdrawn",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "destChainId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "nonce",
        "type": "uint256"
      }
    ],
    "name": "Locked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Paused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldRelayer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newRelayer",
        "type": "address"
      }
    ],
    "name": "RelayerUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "nonce",
        "type": "uint256"
      }
    ],
    "name": "Unlocked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Unpaused",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "collectedFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeBps",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeRecipient",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "destChainId",
        "type": "uint256"
      }
    ],
    "name": "lock",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "destChainId",
        "type": "uint256"
      }
    ],
    "name": "lockNative",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "nonce",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "paused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "processedNonces",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "relayer",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_feeBps",
        "type": "uint256"
      }
    ],
    "name": "setFeeBps",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_feeRecipient",
        "type": "address"
      }
    ],
    "name": "setFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_relayer",
        "type": "address"
      }
    ],
    "name": "setRelayer",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_nonce",
        "type": "uint256"
      }
    ],
    "name": "unlock",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "unpause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "withdrawFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

YOUR GATEWAY TO TUVEN

Connect to possibility.

Choose a wallet. Your assets stay in your control.

No wallet detected. On mobile, open this page in your wallet’s built-in browser.
Get MetaMask

Tuven · Chain 99359 · USDX