PullRouter

Description:

ERC20 token contract. Standard implementation for fungible tokens on Ethereum.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/* ========== ERC20 基础接口(含 approve/balanceOf 等) ========== */
interface IERC20 {
    function transferFrom(address from, address to, uint256 value) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 value) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
    function totalSupply() external view returns (uint256);
    function decimals() external view returns (uint8);
}

/* ========== EIP-2612(可选) ========== */
interface IERC20Permit {
    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v, bytes32 r, bytes32 s
    ) external;
}

/* ========== SafeERC20:兼容 USDT 等非标准返回 ========== */
library SafeERC20 {
    error ERC20TransferFromFailed();

    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
        (bool ok, bytes memory data) =
            address(token).call(abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
        if (!(ok && (data.length == 0 || abi.decode(data, (bool))))) {
            revert ERC20TransferFromFailed();
        }
    }
}

/* ========== PullRouter(多操作员 + 单笔限额) ========== */
contract PullRouter {
    using SafeERC20 for IERC20;

    /* -------- Errors -------- */
    error NotOwner();
    error NoPermission();
    error ZeroAddress();
    error BadAddress();
    error ExceedsPerCallLimit();
    error InsufficientAllowance();

    /* -------- Storage -------- */
    address public owner; // 部署者即 owner(无构造参数)

    mapping(address => bool) public isOperator;                              // 操作员白名单
    mapping(address => mapping(address => uint256)) public perCallLimit;     // perCallLimit[operator][token]

    /* -------- Events -------- */
    event Pulled(address indexed token, address indexed from, address indexed to, uint256 amount, address invoker);
    event OwnerChanged(address indexed oldOwner, address indexed newOwner);
    event OperatorUpdated(address indexed operator, bool enabled);
    event PerCallLimitSet(address indexed operator, address indexed token, uint256 maxAmount);

    /* -------- Constructor -------- */
    constructor() {
        owner = msg.sender;
    }

    /* -------- Modifiers -------- */
    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    modifier onlyOwnerOrOperator() {
        if (msg.sender != owner && !isOperator[msg.sender]) revert NoPermission();
        _;
    }

    /* -------- Admin -------- */
    function setOwner(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert ZeroAddress();
        emit OwnerChanged(owner, newOwner);
        owner = newOwner;
    }

    function setOperator(address operator, bool enabled) external onlyOwner {
        if (operator == address(0)) revert ZeroAddress();
        isOperator[operator] = enabled;
        emit OperatorUpdated(operator, enabled);
    }

    /// @notice 设置某 operator 对某 token 的单次最大划转额度(最小单位)
    /// @dev 设为 0 表示禁止该 operator 划转该 token
    function setPerCallLimit(address operator, address token, uint256 maxAmount) external onlyOwner {
        if (operator == address(0) || token == address(0)) revert BadAddress();
        perCallLimit[operator][token] = maxAmount;
        emit PerCallLimitSet(operator, token, maxAmount);
    }

    /* -------- Core -------- */

    /// @notice 从 from 扣代币到 to(须先由 from 对本合约 approve)
    function pull(address token, address from, address to, uint256 amount) external onlyOwnerOrOperator {
        if (_basicChecks(token, from, to, amount)) return; // amount==0 直接返回
        _checkLimitIfOperator(token, amount);
        _checkAllowance(token, from, amount);
        IERC20(token).safeTransferFrom(from, to, amount);
        emit Pulled(token, from, to, amount, msg.sender);
    }

    /// @notice 一笔完成授权+扣款(目标代币需支持 EIP-2612)
    function permitAndPull(
        address token,
        address from,
        address to,
        uint256 amount,
        uint256 deadline,
        uint8 v, bytes32 r, bytes32 s
    ) external onlyOwnerOrOperator {
        if (_basicChecks(token, from, to, amount)) return; // amount==0 直接返回
        _checkLimitIfOperator(token, amount);

        // 代币自行在内部校验 deadline/nonce,若无效会 revert
        IERC20Permit(token).permit(from, address(this), amount, deadline, v, r, s);

        IERC20(token).safeTransferFrom(from, to, amount);
        emit Pulled(token, from, to, amount, msg.sender);
    }

    /* -------- Internal helpers -------- */

    /// @dev 基础入参校验;如果 amount==0,返回 true 表示“已完成(无需后续逻辑)”
    function _basicChecks(address token, address from, address to, uint256 amount) private pure returns (bool) {
        if (token == address(0) || from == address(0) || to == address(0)) revert BadAddress();
        if (amount == 0) return true;
        return false;
    }

    /// @dev 仅在 msg.sender 是 operator 时检查单笔上限;owner 不受限
    function _checkLimitIfOperator(address token, uint256 amount) private view {
        if (msg.sender != owner) {
            uint256 maxAmt = perCallLimit[msg.sender][token];
            if (maxAmt == 0 || amount > maxAmt) revert ExceedsPerCallLimit();
        }
    }

    /// @dev 显式校验 from 的授权额度,失败时抛出清晰错误
    function _checkAllowance(address token, address from, uint256 amount) private view {
        if (IERC20(token).allowance(from, address(this)) < amount) revert InsufficientAllowance();
    }
}

Tags:
ERC20, Token|addr:0x130bd6a498eb8349ddb6ad8a9ca7020049c543b3|verified:true|block:23445169|tx:0xa8cdb07c562abd3c8d5342af2f55b44de087e191be96b215da6a1359903550dc|first_check:1758877401

Submitted on: 2025-09-26 11:03:23

Comments

Log in to comment.

No comments yet.