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