Contract Name:
BridgeToken
Contract Source Code:
<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IBridgeToken} from "./interfaces/IBridgeToken.sol";
/// @title BridgeToken
/// @notice ERC20 token with mint/burn capabilities for cross-chain bridges
/// @dev Design decisions and tradeoffs:
///
/// TRUST ASSUMPTIONS:
/// - Minters (bridge contracts) are trusted to only mint when valid burns occur
/// - If a minter is compromised, they can mint unlimited tokens
/// - This is why minter management is critical (see setMinter)
///
/// WHY MINT/BURN vs LOCK/UNLOCK?
/// Mint/Burn:
/// + No liquidity fragmentation (tokens exist on one chain at a time)
/// + Total supply remains constant across all chains
/// + Simpler accounting
/// - Requires token to have mint authority (can't use for existing tokens like USDC)
/// - Trust in bridge's mint/burn logic
///
/// Lock/Unlock:
/// + Works with any existing token
/// + No special token permissions needed
/// - Liquidity locked on source chain (capital inefficient)
/// - Wrapped tokens on destination (not fungible with native)
/// - TVL in bridge contract = honeypot for hackers
contract BridgeToken is IBridgeToken {
string public name;
string public symbol;
uint8 public constant decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
/// @notice Addresses authorized to mint tokens
/// @dev Critical security surface - compromise = unlimited minting
mapping(address => bool) public isMinter;
/// @notice Contract owner who can manage minters
address public owner;
/// @notice Pending owner for 2-step ownership transfer
/// @dev 2-step prevents accidentally transferring to wrong address
address public pendingOwner;
event MinterSet(address indexed minter, bool authorized);
event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
error Unauthorized();
error ZeroAddress();
error InsufficientBalance();
error InsufficientAllowance();
modifier onlyOwner() {
if (msg.sender != owner) revert Unauthorized();
_;
}
modifier onlyMinter() {
if (!isMinter[msg.sender]) revert Unauthorized();
_;
}
constructor(string memory _name, string memory _symbol) {
name = _name;
symbol = _symbol;
owner = msg.sender;
}
// ============ Minter Management ============
/// @notice Authorize or revoke a minter
/// @dev SECURITY: This is the most critical function
/// - Should be behind timelock in production
/// - Consider multisig ownership
/// - Monitor for unexpected minter additions
function setMinter(address minter, bool authorized) external onlyOwner {
if (minter == address(0)) revert ZeroAddress();
isMinter[minter] = authorized;
emit MinterSet(minter, authorized);
}
// ============ Ownership Transfer ============
/// @notice Start ownership transfer (2-step process)
/// @dev Why 2-step? Prevents losing ownership to typos or wrong addresses
function transferOwnership(address newOwner) external onlyOwner {
pendingOwner = newOwner;
emit OwnershipTransferStarted(owner, newOwner);
}
/// @notice Accept ownership transfer
function acceptOwnership() external {
if (msg.sender != pendingOwner) revert Unauthorized();
emit OwnershipTransferred(owner, msg.sender);
owner = msg.sender;
pendingOwner = address(0);
}
// ============ Mint/Burn (Bridge Operations) ============
/// @notice Mint tokens to recipient
/// @dev Called by bridge when tokens are burned on source chain
/// TRUST: We trust the bridge verified the burn before calling
function mint(address to, uint256 amount) external onlyMinter {
if (to == address(0)) revert ZeroAddress();
totalSupply += amount;
balanceOf[to] += amount;
emit Transfer(address(0), to, amount);
}
/// @notice Burn tokens from caller
/// @dev User initiates bridge by burning their tokens
function burn(uint256 amount) external {
if (balanceOf[msg.sender] < amount) revert InsufficientBalance();
balanceOf[msg.sender] -= amount;
totalSupply -= amount;
emit Transfer(msg.sender, address(0), amount);
}
/// @notice Burn tokens from address (requires approval)
/// @dev Bridge contract can burn on behalf of user
function burnFrom(address from, uint256 amount) external {
uint256 currentAllowance = allowance[from][msg.sender];
if (currentAllowance < amount) revert InsufficientAllowance();
if (balanceOf[from] < amount) revert InsufficientBalance();
allowance[from][msg.sender] = currentAllowance - amount;
balanceOf[from] -= amount;
totalSupply -= amount;
emit Transfer(from, address(0), amount);
}
// ============ Standard ERC20 ============
function transfer(address to, uint256 amount) external returns (bool) {
if (to == address(0)) revert ZeroAddress();
if (balanceOf[msg.sender] < amount) revert InsufficientBalance();
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
if (to == address(0)) revert ZeroAddress();
uint256 currentAllowance = allowance[from][msg.sender];
if (currentAllowance < amount) revert InsufficientAllowance();
if (balanceOf[from] < amount) revert InsufficientBalance();
allowance[from][msg.sender] = currentAllowance - amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IERC20} from "forge-std/interfaces/IERC20.sol";
/// @title IBridgeToken
/// @notice Interface for tokens that can be minted/burned by a bridge
/// @dev Key design decision: Who has mint/burn authority?
/// - Option 1: Single bridge contract (simpler, but single point of failure)
/// - Option 2: Multiple authorized bridges (more flexible, but harder access control)
/// - Option 3: Governance-controlled minter list (most decentralized, but slower to update)
interface IBridgeToken is IERC20 {
/// @notice Mints tokens to a recipient
/// @dev Only callable by authorized minters (bridge contracts)
/// @param to The recipient address
/// @param amount The amount to mint
function mint(address to, uint256 amount) external;
/// @notice Burns tokens from the caller
/// @dev Anyone can burn their own tokens
/// @param amount The amount to burn
function burn(uint256 amount) external;
/// @notice Burns tokens from an address (requires approval)
/// @dev Used by bridge contracts to burn tokens on behalf of users
/// @param from The address to burn from
/// @param amount The amount to burn
function burnFrom(address from, uint256 amount) external;
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.13 <0.9.0;
/// @dev Interface of the ERC20 standard as defined in the EIP.
/// @dev This includes the optional name, symbol, and decimals metadata.
interface IERC20 {
/// @dev Emitted when `value` tokens are moved from one account (`from`) to another (`to`).
event Transfer(address indexed from, address indexed to, uint256 value);
/// @dev Emitted when the allowance of a `spender` for an `owner` is set, where `value`
/// is the new allowance.
event Approval(address indexed owner, address indexed spender, uint256 value);
/// @notice Returns the amount of tokens in existence.
function totalSupply() external view returns (uint256);
/// @notice Returns the amount of tokens owned by `account`.
function balanceOf(address account) external view returns (uint256);
/// @notice Moves `amount` tokens from the caller's account to `to`.
function transfer(address to, uint256 amount) external returns (bool);
/// @notice Returns the remaining number of tokens that `spender` is allowed
/// to spend on behalf of `owner`
function allowance(address owner, address spender) external view returns (uint256);
/// @notice Sets `amount` as the allowance of `spender` over the caller's tokens.
/// @dev Be aware of front-running risks: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
function approve(address spender, uint256 amount) external returns (bool);
/// @notice Moves `amount` tokens from `from` to `to` using the allowance mechanism.
/// `amount` is then deducted from the caller's allowance.
function transferFrom(address from, address to, uint256 amount) external returns (bool);
/// @notice Returns the name of the token.
function name() external view returns (string memory);
/// @notice Returns the symbol of the token.
function symbol() external view returns (string memory);
/// @notice Returns the decimals places of the token.
function decimals() external view returns (uint8);
}