Robinhood MainnetChain ID: 4663
Contract detail

ArbExecutor

ArbExecutor is a verified Robinhood Chain smart contract at 0x36ac30af…bbf79b4a with 23 indexed transactions.

Address type
Smart contract
Transactions
23
First indexed block
#28369659
Latest indexed block
#29473958
Indexed token transfers
9
ERC-20 transfers
9
Emitted events
1
Verification
Verified
Compiler
v0.8.28+commit.7893614a
Verification match
full
Creation transaction
0x8cc0db39…6ec8d413
Created at block
#28369659
Language
solidity
EVM version
prague
License
none
Compiler optimization
Enabled · 1000000 runs
Related activity

Transactions

Verified contract

Contract source code

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.28;

/// @notice Uniswap V3 池接口,只声明我们真正调用的部分。
interface IUniswapV3Pool {
    function swap(
        address recipient,
        bool zeroForOne,
        int256 amountSpecified,
        uint160 sqrtPriceLimitX96,
        bytes calldata data
    ) external returns (int256 amount0, int256 amount1);
}

interface IERC20 {
    function balanceOf(address) external view returns (uint256);
    function transfer(address, uint256) external returns (bool);
}

/// @title ArbExecutor —— Robinhood Chain 原子环路套利执行器
///
/// # 设计要点(DEV_PLAN §7.1 / HANDOFF §6)
///
/// 1. **利润校验以 BASE 计价**,不是检查某一跳的输出数量。
///    中间跳数量对了不代表闭环赚钱。这是「不成交不亏钱」成立的唯一前提。
///
/// 2. **直接调 `pool.swap()`,不走 SwapRouter02。** 省 gas、省一层外部调用,
///    而且拿到 `uniswapV3SwapCallback` 才能把下一跳串起来。
///
/// 3. **用 V3 回调链串起整个环 —— 这不是闪电贷,是回调机制本身。**
///    嵌套顺序是关键:每个回调里**先做下一跳、再还自己的债**。
///    最内层那跳产出的 BASE,正好用来还最外层第一跳欠的 BASE。
///    ```
///    P1.swap(BASE->T1) ─┐ 欠 P1 amountIn 的 BASE
///                       └→ P2.swap(T1->T2) ─┐ 欠 P2 的 T1
///                                           └→ P3.swap(T2->BASE) ─┐ 欠 P3 的 T2
///                                                                 └→ 还 P3(手里有 T2)
///                                            ← 还 P2(手里有 T1)
///                       ← 还 P1(手里有刚换回来的 BASE)✅ 零垫资
///    ```
///    → **中间跳完全不需要库存**,只有最终利润落袋。
///
/// 4. **双密钥**:owner(冷钥)能 `sweep` / 改白名单;executor(热钥)只能 `exec`。
///    服务器被攻破的最坏后果是「攻击者替你跑套利」,不是「资金被卷走」。
///
/// 5. **不做可升级代理。** 重新部署便宜且更安全,代理只是多一个攻击面。
contract ArbExecutor {
    /// 每一跳在 path 里占的字节数:pool(20) + tokenIn(20) + dir(1)
    uint256 internal constant HOP_SIZE = 41;

    /// TickMath 的价格边界。传这两个值等于「不限价」——
    /// 环路套利不需要限价,投入量本身就是约束。
    uint160 internal constant MIN_SQRT_RATIO_PLUS_ONE = 4295128740;
    uint160 internal constant MAX_SQRT_RATIO_MINUS_ONE =
        1461446703485210103287273052203988822378723970341;

    address public immutable owner;

    /// 热钥白名单。只能调 `exec`,**无法提走任何资金**。
    mapping(address => bool) public execs;

    /// 允许出现在 path 里的池。
    ///
    /// 严格说 §7.1 的利润校验已经保证了 BASE 不会净流出,但这层白名单
    /// 额外挡住两件事:① 热钥被攻破后被引导去调用任意合约;
    /// ② 运维手滑填错一个池地址。v1 只有 8 个池,维护成本可以忽略。
    mapping(address => bool) public pools;

    /// 当前允许回调的池。**这是回调鉴权的核心。**
    ///
    /// 只检查「msg.sender 是不是白名单里的池」是不够的:
    /// 任何人都能直接调 `uniswapV3SwapCallback` 并伪造 data。
    /// 所以每次 `swap` 前把目标池写进这里,回调进来先比对 ——
    /// 只有我们**刚刚主动调用**的那个池才能回调成功。
    address internal _callbackPool;

    error NotOwner();
    error NotExec();
    error BadPath();
    error UnknownPool(address pool);
    error BadCallback();
    error NoProfit(uint256 balanceBefore, uint256 balanceAfter, uint256 minProfit);

    event Executed(address indexed base, uint256 amountIn, uint256 profit);

    constructor(address _owner) {
        owner = _owner;
    }

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

    // ------------------------------------------------------------ 冷钥操作

    function setExec(address who, bool ok) external onlyOwner {
        execs[who] = ok;
    }

    function setPool(address pool, bool ok) external onlyOwner {
        pools[pool] = ok;
    }

    function setPools(address[] calldata list, bool ok) external onlyOwner {
        for (uint256 i = 0; i < list.length; ++i) {
            pools[list[i]] = ok;
        }
    }

    /// 提走资金。**只有冷钥能调** —— 热钥泄露不会导致资金损失。
    function sweep(address token, address to, uint256 amount) external onlyOwner {
        IERC20(token).transfer(to, amount);
    }

    // ------------------------------------------------------------ 热路径

    /// 执行一条环路。
    ///
    /// @param path  打包字节:每跳 `pool(20) ++ tokenIn(20) ++ dir(1)`。
    ///              `dir` 非零表示 `zeroForOne`。第一跳的 `tokenIn` 即 BASE。
    /// @param amountIn   投入的 BASE 数量(精确输入)。
    /// @param minProfit  以 BASE 计价的最小净利。**不达标即 revert。**
    function exec(bytes calldata path, uint256 amountIn, uint256 minProfit) external {
        if (!execs[msg.sender]) revert NotExec();
        uint256 hops = path.length / HOP_SIZE;
        if (hops < 2 || path.length % HOP_SIZE != 0) revert BadPath();

        address base = _tokenIn(path, 0);
        uint256 balBefore = IERC20(base).balanceOf(address(this));

        _swap(path, 0, amountIn);

        uint256 balAfter = IERC20(base).balanceOf(address(this));
        // 唯一真正的护栏:以 BASE 计价,闭环必须净增 minProfit 以上。
        // 用加法而不是减法比较,避免 balAfter < balBefore 时下溢
        // (0.8 会 revert,但错误信息不如这里明确)。
        if (balAfter < balBefore + minProfit) {
            revert NoProfit(balBefore, balAfter, minProfit);
        }

        // 回调游标复位:留着一个非零值等于给下一笔交易留了一个可用的回调口子。
        _callbackPool = address(0);

        emit Executed(base, amountIn, balAfter - balBefore);
    }

    /// 发起第 `i` 跳。
    function _swap(bytes calldata path, uint256 i, uint256 amountIn) internal {
        (address pool, , bool zeroForOne) = _hop(path, i);
        if (!pools[pool]) revert UnknownPool(pool);

        // 先登记「接下来只接受这个池的回调」,再发起 swap。
        _callbackPool = pool;

        IUniswapV3Pool(pool).swap(
            address(this),
            zeroForOne,
            int256(amountIn), // 正数 = exact input
            zeroForOne ? MIN_SQRT_RATIO_PLUS_ONE : MAX_SQRT_RATIO_MINUS_ONE,
            abi.encode(path, i)
        );
    }

    /// V3 回调。**整个环路的递归就发生在这里。**
    function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes calldata data
    ) external {
        // 鉴权:必须是我们刚刚主动调用的那个池。
        // 少了这一条,任何人都能伪造 data 骗走代币。
        if (msg.sender != _callbackPool) revert BadCallback();

        (bytes memory path, uint256 i) = abi.decode(data, (bytes, uint256));
        (address pool, address tokenIn, bool zeroForOne) = _hopMemory(path, i);
        // data 里的池必须与实际回调者一致,防止用别的跳的 data 来串场
        if (pool != msg.sender) revert BadCallback();

        // 正的 delta 是我们欠池子的;负的是池子付给我们的。
        uint256 owed = uint256(zeroForOne ? amount0Delta : amount1Delta);
        uint256 received = uint256(-(zeroForOne ? amount1Delta : amount0Delta));

        uint256 hops = path.length / HOP_SIZE;
        if (i + 1 < hops) {
            // 关键顺序:**先做下一跳,再还自己的债。**
            // 下一跳(乃至整条链)产出的资金正是还债的来源,
            // 这就是「中间跳零垫资」的由来。
            _swapMemory(path, i + 1, received);
        }

        // 还债。走到这里时手里一定有 tokenIn:
        // - 非最后一跳:上一跳刚把它换给我们
        // - 最后一跳:它就是环路的产出,此刻已在手上
        IERC20(tokenIn).transfer(msg.sender, owed);
    }

    /// `_swap` 的 memory 版本(回调里 path 已经是 memory)。
    function _swapMemory(bytes memory path, uint256 i, uint256 amountIn) internal {
        (address pool, , bool zeroForOne) = _hopMemory(path, i);
        if (!pools[pool]) revert UnknownPool(pool);

        _callbackPool = pool;

        IUniswapV3Pool(pool).swap(
            address(this),
            zeroForOne,
            int256(amountIn),
            zeroForOne ? MIN_SQRT_RATIO_PLUS_ONE : MAX_SQRT_RATIO_MINUS_ONE,
            abi.encode(path, i)
        );
    }

    // ------------------------------------------------------------ path 解码

    function _hop(bytes calldata path, uint256 i)
        internal
        pure
        returns (address pool, address tokenIn, bool zeroForOne)
    {
        uint256 o = i * HOP_SIZE;
        pool = address(bytes20(path[o:o + 20]));
        tokenIn = address(bytes20(path[o + 20:o + 40]));
        zeroForOne = uint8(path[o + 40]) != 0;
    }

    function _tokenIn(bytes calldata path, uint256 i) internal pure returns (address) {
        uint256 o = i * HOP_SIZE + 20;
        return address(bytes20(path[o:o + 20]));
    }

    function _hopMemory(bytes memory path, uint256 i)
        internal
        pure
        returns (address pool, address tokenIn, bool zeroForOne)
    {
        uint256 o = i * HOP_SIZE;
        assembly {
            let p := add(add(path, 0x20), o)
            pool := shr(96, mload(p))
            tokenIn := shr(96, mload(add(p, 20)))
            zeroForOne := iszero(iszero(byte(0, mload(add(p, 40)))))
        }
    }
}