Skip to content

Instantly share code, notes, and snippets.

@YukiCoco
Created May 3, 2026 23:12
Show Gist options
  • Select an option

  • Save YukiCoco/71e9833fc25c9bf27d95fd8a41e7f18c to your computer and use it in GitHub Desktop.

Select an option

Save YukiCoco/71e9833fc25c9bf27d95fd8a41e7f18c to your computer and use it in GitHub Desktop.
Sato 协议链上分析报告

Sato 协议链上分析报告

分析对象:Sato(以太坊主网,2026‑05‑03 上线) 分析时间:2026‑05‑04(链上块高 ~25,017,544) 分析者:基于 Blockscout PRO API + NodeReal RPC + 已验证源码 + opcode‑aware 字节码扫描


1. TL;DR

Sato 是一个部署在 Uniswap V4 上的算法发行 meme 代币,由三件合约协同工作:

角色 地址 验证
SatoToken(ERC‑20) 0x829f4B62EEBE12Af653b4dD4fFc480966F7d7f09
SatoHook(V4 hook,唯一 minter) 0x0000f07d2B5F1Ddf3244b8780F972f306EFd2888
SatoSwapRouter(用户入口) 0x06A645079cd4F3Bb38FfaD47f92180B8041145E3
Uniswap V4 PoolManager(基础设施) 0x000000000004444c5dc75cB358380D2e3dE08A90 官方
部署者(EOA) 0xC1d3Bdf90a3A61281D31E9aE25e1153Fb488c3EC

核心结论:链上证据显示项目方已经放弃了所有可执行权限——

  • SatoToken 的 minter 已经一次性永久锁定为 SatoHook;
  • SatoHook 没有任何 admin 路径、不可升级、不可销毁、手续费不可提取;
  • Pool 层 beforeAddLiquidity 永远 revert,任何人(包括项目方)都不能添加/撤回 LP;
  • 三个合约的字节码均无 DELEGATECALL / SELFDESTRUCT / CREATE2 后门。

剩余风险仅是代码逻辑风险(曲线数学、cooldown 边界等),不存在 rug / 暂停 / 黑名单 / 增发税收等中心化风险。


2. 系统架构

┌─────────────────┐ swap/buy ┌──────────────────┐ unlock ┌────────────────┐
│   用户 EOA      │─────────▶│ SatoSwapRouter   │───────▶│ V4 PoolManager │
└─────────────────┘          └──────────────────┘        └────────┬───────┘
                                                                  │ beforeSwap
                                                                  ▼
                                                          ┌──────────────────┐
                                                          │   SatoHook       │
                                                          │  (sole minter)   │
                                                          └────────┬─────────┘
                                                                   │ mint / burn
                                                                   ▼
                                                          ┌──────────────────┐
                                                          │   SatoToken      │
                                                          └──────────────────┘

2.1 设计精髓 ——「全 hook 撮合,零 LP」

源码顶部的实现注释明确说明:项目方采用 Approach B (BeforeSwapDelta) 而非传统的"mint 进储备 + sync/settle"模式。原因是 V4 的 AMM 数学完全基于 Pool.State.liquidity,而 hook 通过 beforeAddLiquidity 永久封禁加 LP,于是 pool 永远无流动性,AMM 无法定价。

最终方案:hook 直接接管整笔 swap,作为对手方:

  • 买(ETH→SATO):通过正向 bonding curve 铸造 SATO 给买家;
  • 卖(SATO→ETH):通过反向曲线,按合约自身储备的 ETH 给卖家;
  • 0.3% LP fee:双向都被 hook 抽走,累计在 feesAccrued 中——但没有提取入口,事实上焚毁。

3. SatoToken 详细分析

3.1 源码关键片段(链上已验证,MIT 协议,Solidity 0.8.26)

contract SatoToken is ERC20 {
    address public minter;
    address public immutable DEPLOYER;
    bool public immutable RESTRICTIONS_FORBIDDEN = true;
    uint256 public immutable GENESIS_BLOCK;
    bytes32 public immutable GENESIS_HASH;

    constructor() ERC20("sato", "sato") {
        DEPLOYER = msg.sender;
        GENESIS_BLOCK = block.number;
        GENESIS_HASH = blockhash(block.number - 1);
    }

    function setMinter(address newMinter) external {
        if (msg.sender != DEPLOYER)        revert NotDeployer();
        if (minter != address(0))          revert MinterAlreadySet();  // ← 一次性锁定
        if (newMinter == address(0))       revert MinterIsZero();
        minter = newMinter;
        emit MinterLocked(newMinter);
    }

    function mint(address to, uint256 amount) external {
        if (msg.sender != minter) revert NotMinter();
        _mint(to, amount);
    }

    function burn(address from, uint256 amount) external {
        if (msg.sender != minter) revert NotMinter();
        _burn(from, amount);
    }
}

显式声明:「No owner. No pause. No blacklist. No fee logic. No upgrade path.

3.2 链上状态(2026‑05‑04)

调用 返回值 说明
minter() 0x0000f07d…2888 = SatoHook,永久锁定
DEPLOYER() 0xC1d3Bd…c3EC 已无任何剩余权限
RESTRICTIONS_FORBIDDEN() true 自我承诺标记
GENESIS_BLOCK() 25,015,094 与 Hook 同块部署
GENESIS_HASH() 0xdbb34f…79f4 与 Hook 完全一致
totalSupply() 15,051,295.22 距 K_SUPPLY (21M) 71.7%

3.3 事件验证

链上 eth_getLogs 全量扫描确认:

  • MinterLocked(address) 事件 count = 1
  • 唯一一次发生在 block 25,015,096,tx 0x7303e30…fd60c
  • indexed minter = 0x0000f07d…2888(SatoHook)

下次再调 setMinter 必然 revert MinterAlreadySet(),DEPLOYER 此后等同普通 EOA。

3.4 字节码扫描(opcode‑aware,已跳过 PUSH 数据段)

size = 2474 bytes
SELFDESTRUCT: 0    DELEGATECALL: 0    CREATE2: 0    CALLCODE: 0
CALL: 0            STATICCALL: 0

任何外部调用都没有,更别提升级或销毁的能力


4. SatoHook 详细分析

4.1 锁定参数(constant,写入字节码不可改)

常量 含义
K_SUPPLY 21,000,000e18 曲线渐近上限
S 500 ether 曲线尺度因子
MAX_BUY_WEI 5 ether 单笔买入上限
COOLDOWN_BLOCKS 1 同钱包"上次买入与首次卖出"间隔
POOL_FEE 3000 (0.3%) V4 hundredths‑of‑bips
ENTROPY_BLOCKS 100 熵窗口(部署后 100 块内有 ±10% 随机奖励)
EXHAUSTION_THRESHOLD 99/100 自废止触发线
FEE_NUMERATOR / DENOMINATOR 30 / 10000 0.3% LP fee

4.2 状态变量(storage)

变量 类型 当前值 是否可写回
_GENESIS_MESSAGE bytes32[8] (private) constructor 写入后永不修改 永不
ethCum uint256 630.67 ETH _executeBuy / _executeSell
totalMintedFair uint256 15,051,294.26 仅 buy/sell
selfDeprecated bool false 单向,写 true 后不可逆
poolInitialized bool true 单向
lastBuyBlock[addr] mapping _executeBuy 中由当前 swapper 写自己
feesAccrued uint256 11.71 ETH 只增不减(无提取入口

4.3 完整 ABI 检查(关键证据)

按状态可变性分类:

类型 数量 说明
19 个 view getter(常量 + 状态查询) 安全
8 个 pure 默认实现(V4 强制接口,未启用) 安全
3 个 nonpayablebeforeInitialize / beforeAddLiquidity / beforeSwap 全部 onlyPoolManager
setOwner / setFee / withdraw / rescue / pause / upgrade / mint(对外) 不存在

beforeAddLiquidity 实现是一行 revert LiquidityAdditionsForbidden();——永久禁止任何人加 LP

4.4 资金流路径分析

_executeBuy

fee       = ethIn * 30 / 10000
ethToCurve = ethIn - fee
mintAmount = Curve.mintFor(ethCum, ethToCurve) * entropy(0.9~1.1)

POOL_MANAGER.sync(SATO_CURRENCY)
SATO_TOKEN.mint(address(POOL_MANAGER), mintAmount)   // SATO 给买家
POOL_MANAGER.settle()
POOL_MANAGER.take(ETH_CURRENCY, address(this), ethIn) // ETH 进入 hook

ethCum          += ethToCurve
totalMintedFair += fairSato
feesAccrued     += fee     // ← 永久锁死
lastBuyBlock[swapper] = block.number

_executeSell

require(block.number - lastBuyBlock[swapper] >= 1) // cooldown
satoFairIn = satoIn * totalMintedFair / actualSupply
ethRaw     = Curve.burnFor(totalMintedFair, satoFairIn)
fee        = ethRaw * 30 / 10000
ethOut     = ethRaw - fee
require(address(this).balance >= ethOut)

POOL_MANAGER.take(SATO_CURRENCY, address(this), satoIn)
SATO_TOKEN.burn(address(this), satoIn)             // 销毁 SATO
POOL_MANAGER.settle{value: ethOut}()               // ETH 给卖家

ethCum          -= ethRaw   // 或 = 0
totalMintedFair -= satoFairIn
feesAccrued     += fee      // ← 永久锁死

关键事实:所有 CALL 的目标都是 immutable(POOL_MANAGER / SATO_TOKEN)或 address(this)没有任何用户可控的外部调用目标

4.5 字节码扫描

size = 8576 bytes
SELFDESTRUCT: 0    DELEGATECALL: 0    CREATE2: 0    CALLCODE: 0
CALL: 7  (全部指向 PoolManager 或 SatoToken)
STATICCALL: 1
  • 0 DELEGATECALL → 无任何升级/代理跳转的可能;
  • 0 SELFDESTRUCT → 合约不可销毁;
  • 0 CREATE2 → 不会孵化子合约。

4.6 部署完整性

SatoToken 与 SatoHook 链上读取的 GENESIS_BLOCK = 25,015,094GENESIS_HASH = 0xdbb34f…79f4 完全一致——证明两者在同一区块原子部署,没有"先部署一个空壳再替换 minter"的可能性。

部署使用 Arachnid 的 deterministic CREATE2 deployer (0x4e59b4…956C),hook 地址前缀 0x0000… 是为了把 V4 hook permission flag 编码进低 14 位(V4 协议要求),不是单纯的 vanity。


5. SatoSwapRouter 详细分析

最薄的一层:~120 行 Solidity,仅作为用户友好的入口。

contract SatoSwapRouter is IUnlockCallback {
    IPoolManager public immutable manager;

    function buy(PoolKey, swapper, recipient) payable returns (uint256)
    function sell(PoolKey, swapper, recipient, satoIn) returns (uint256)
    function unlockCallback(bytes) returns (bytes)   // 仅 manager 可调
    receive() external payable {}
}
  • 无 owner / 无 admin / 无升级 / 无 receive 提款;
  • unlockCallbackif (msg.sender != address(manager)) revert NotManager() 守住;
  • 字节码:0 DELEGATECALL / 0 SELFDESTRUCT / 0 CREATE2
  • 当前余额 0.064 ETH 是历史 dust,无人能取出(但也无人能多放进去)。

由于 swapper 由 router 调用方自由指定(写进 hookData 用作 cooldown 标记),用户可以让别人替自己代付 gas 买入。这是设计选择,不是漏洞。


6. 当前链上状态快照

指标 含义
区块高度 ~25,017,544 部署后约 2,450 块(~8 小时)
SATO.totalSupply 15,051,295.22 占 K_SUPPLY 21M 的 71.7%
Hook.totalMintedFair 15,051,294.26 与 totalSupply 比值 ≈ 1.000000064,熵奖励残值极小
Hook.ethCum 630.67 ETH 反向曲线 ETH 储备
Hook.feesAccrued 11.71 ETH 永久锁死
Hook.curveReserveEth 630.67 ETH = balance − feesAccrued
Hook.selfDeprecated false 触发阈值 ≈ 20.79M(99% × 21M)
距 self‑deprecate ~5,748,706 SATO 待铸造 之后将永久禁止再买
持有人数 1,414
熵窗口 已结束 GENESIS_BLOCK + 100 = 25,015,194,已过约 2,350 块

近 50 笔交易统计:

  • 37 笔 buy / 13 笔 sell
  • 33 个独立买家、12 个独立卖家
  • 累计买入 ~9.85 ETH(最近 6 分钟)

7. 权限放弃验证矩阵(最终)

风险类别 想象的攻击路径 实际堵死方式 验证手段
增发 项目方调 mint minter ≠ DEPLOYER;minter 已锁定为 hook;hook 的 mint 仅 beforeSwap 内部调 源码 + eth_call + log 扫描
改 minter DEPLOYER 再调 setMinter MinterAlreadySet 守卫;MinterLocked 事件 count=1 源码 + eth_getLogs
加税 / 黑名单 / 暂停 owner 改逻辑 无 owner,OZ 标准 ERC20 + RESTRICTIONS_FORBIDDEN=true 源码 + ABI 完整列举
升级合约 后置部署新实现 非 proxy;字节码 0 DELEGATECALL;无 setImpl proxy_type=null + opcode 扫描
销毁合约 SELFDESTRUCT 后跑路 字节码 0 SELFDESTRUCT;Cancun 后即使有也只清 balance opcode 扫描
撤池 / 撤 LP 撤回 V4 LP NFT beforeAddLiquidity 永远 revert;pool liquidity 恒为 0 源码 + ABI
提手续费 withdraw fees hook 没有任何函数减少 feesAccrued 或转出 ETH(除卖家正常成交) 源码 grep
操纵 cooldown 跳过反夹 lastBuyBlock _executeBuy 中由当前 swapper 写自己 源码
反向曲线储备被偷 任意外部调用 7 个 CALL 全部指向 immutable 目标 opcode 扫描 + 源码追踪

8. 剩余风险清单

不属于权限风险,但仍需自行评估:

  1. 曲线数学正确性Curve.solmintFor / burnFor)的实现没有审计;如果反向曲线在边界情况(极小或极大 satoIn)下计算错误,可能导致 ethCum 漂移。
  2. 市场风险:MAX_BUY_WEI = 5 ETH 的限制让大户必须分多笔吃单,但不能阻止聚合交易者持续砸盘。
  3. 熵机制溯源:熵窗口已结束(部署后 100 块内),但当时熵奖励基于 blockhash(block.number-1),可被矿工/builder 操纵(窗口已过去,影响已固化)。
  4. PoolManager 信任:依赖 Uniswap V4 官方 PoolManager 的安全性(业界基础设施级别)。
  5. 手续费永久锁定:11.7 ETH 已经永远死在 hook 里,未来还会持续累积。这是设计选择("焚毁手续费"),不是 bug,但用户需要知道这部分价值不会回流给任何人。
  6. philosophy() 是项目方的 manifesto 字符串:未在本报告中解码,可独立 eth_call 0x2d37adac 查看,不影响安全性结论。

9. 结论

Sato 协议在权限维度已经做到了一个 ERC‑20 + Uniswap V4 Hook 组合在结构上能达到的最强放弃形态。

部署者保留的唯一一次性权限(setMinter)已在 block 25,015,096 被使用并锁死;其后没有任何人(包括部署者本人)能修改代币行为、暂停交易、撤池、提取手续费或升级合约。

系统由代码完全控制,且代码不可变。剩余风险全部是业务逻辑风险而非信任风险


附录 A:所有可独立复现的链上验证命令

TOKEN=0x829f4B62EEBE12Af653b4dD4fFc480966F7d7f09
HOOK=0x0000f07d2B5F1Ddf3244b8780F972f306EFd2888
ROUTER=0x06A645079cd4F3Bb38FfaD47f92180B8041145E3
RPC=https://eth-mainnet.nodereal.io/v1/$NODEREAL_API_KEY

# 1. 验证 SATO minter 已锁
cast call $TOKEN "minter()(address)" --rpc-url $RPC
# → 0x0000f07d2B5F1Ddf3244b8780F972f306EFd2888

# 2. 验证 MinterLocked 事件只发了一次
cast logs --address $TOKEN \
  --from-block 25015000 --to-block latest \
  "MinterLocked(address)" --rpc-url $RPC | grep -c "blockHash"
# → 1

# 3. 验证 Hook 和 Token 同块部署
cast call $TOKEN "GENESIS_BLOCK()(uint256)" --rpc-url $RPC
cast call $HOOK  "GENESIS_BLOCK()(uint256)" --rpc-url $RPC
# 两者都是 25015094

# 4. 验证 Hook 池已初始化、未自废
cast call $HOOK "poolInitialized()(bool)"  --rpc-url $RPC   # true
cast call $HOOK "selfDeprecated()(bool)"   --rpc-url $RPC   # false

# 5. 验证 Hook 的 CALL 全部指向已知地址
#    (需要 disasm 或 4byte 检索;Blockscout 上看 source code 即可)

# 6. 字节码扫描可直接执行本报告 §4.5 中的 Python 片段

附录 B:关键交易

时间 (UTC) 用途 tx
2026‑05‑03 14:33:47 CREATE2 部署 SatoToken + SatoHook(同块) 0x...4e59b4...
2026‑05‑03 14:34:23 部署者调用 SatoToken.setMinter(SatoHook) ← 一次性锁权 block 25,015,096
2026‑05‑03 14:34:35 部署者调用 PoolManager.initialize(...)
2026‑05‑03 14:40:11 部署者部署 SatoSwapRouter tx 0xe294c5d1…7c091
2026‑05‑03 14:42:59 第一笔买入(部署者自买 0.01 ETH)

生成于 2026‑05‑04,所有链上数据来自 Blockscout PRO API + NodeReal Ethereum mainnet RPC,所有源码引用来自 Blockscout 已验证发布版本。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment