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 success, bytes memory data) =
            address(token).call(abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
        if (!(success && (data.length == 0 || abi.decode(data, (bool))))) {
            revert ERC20TransferFromFailed();
        }
    }
}

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

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

    /* State */
    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:owner=部署者 */
    constructor() {
        owner = msg.sender;
    }

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

    /* ---- 管理 ---- */
    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);
    }

    /* ---- 业务 ---- */

    /// @notice 从 from 扣代币到 to(须先由 from 对本合约 approve)
    function pull(address token, address from, address to, uint256 amount) external onlyOwnerOrOperator {
        if (token == address(0) || from == address(0) || to == address(0)) revert BadAddress();
        if (amount == 0) return;

        if (msg.sender != owner) {
            uint256 maxAmt = perCallLimit[msg.sender][token];
            if (maxAmt == 0 || amount > maxAmt) revert ExceedsPerCallLimit();
        }

        if (IERC20(token).allowance(from, address(this)) < amount) revert InsufficientAllowance();

        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 (token == address(0) || from == address(0) || to == address(0)) revert BadAddress();
        if (amount == 0) return;

        if (msg.sender != owner) {
            uint256 maxAmt = perCallLimit[msg.sender][token];
            if (maxAmt == 0 || amount > maxAmt) revert ExceedsPerCallLimit();
        }

        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);
    }
}

Tags:
ERC20, Token|addr:0x38dbae131c21af99c49d68edd002b71f322c601c|verified:true|block:23444728|tx:0xe92fc96a1eca07a29a8552e0f3b365c0371f81352174e41a0e50c32be6cc0664|first_check:1758876970

Submitted on: 2025-09-26 10:56:13

Comments

Log in to comment.

No comments yet.