分析对象:Sato(以太坊主网,2026‑05‑03 上线) 分析时间:2026‑05‑04(链上块高 ~25,017,544) 分析者:基于 Blockscout PRO API + NodeReal RPC + 已验证源码 + opcode‑aware 字节码扫描
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 / 暂停 / 黑名单 / 增发税收等中心化风险。
┌─────────────────┐ swap/buy ┌──────────────────┐ unlock ┌────────────────┐
│ 用户 EOA │─────────▶│ SatoSwapRouter │───────▶│ V4 PoolManager │
└─────────────────┘ └──────────────────┘ └────────┬───────┘
│ beforeSwap
▼
┌──────────────────┐
│ SatoHook │
│ (sole minter) │
└────────┬─────────┘
│ mint / burn
▼
┌──────────────────┐
│ SatoToken │
└──────────────────┘
源码顶部的实现注释明确说明:项目方采用 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中——但没有提取入口,事实上焚毁。
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.」
| 调用 | 返回值 | 说明 |
|---|---|---|
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% |
链上 eth_getLogs 全量扫描确认:
MinterLocked(address)事件 count = 1- 唯一一次发生在 block 25,015,096,tx
0x7303e30…fd60c - indexed minter =
0x0000f07d…2888(SatoHook)
下次再调 setMinter 必然 revert MinterAlreadySet(),DEPLOYER 此后等同普通 EOA。
size = 2474 bytes
SELFDESTRUCT: 0 DELEGATECALL: 0 CREATE2: 0 CALLCODE: 0
CALL: 0 STATICCALL: 0
任何外部调用都没有,更别提升级或销毁的能力。
| 常量 | 值 | 含义 |
|---|---|---|
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 |
| 变量 | 类型 | 当前值 | 是否可写回 |
|---|---|---|---|
_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 | 只增不减(无提取入口) |
按状态可变性分类:
| 类型 | 数量 | 说明 |
|---|---|---|
19 个 view getter(常量 + 状态查询) |
安全 | |
8 个 pure 默认实现(V4 强制接口,未启用) |
安全 | |
3 个 nonpayable:beforeInitialize / beforeAddLiquidity / beforeSwap |
全部 onlyPoolManager |
|
setOwner / setFee / withdraw / rescue / pause / upgrade / mint(对外) |
❌ 不存在 |
beforeAddLiquidity 实现是一行 revert LiquidityAdditionsForbidden();——永久禁止任何人加 LP。
_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),没有任何用户可控的外部调用目标。
size = 8576 bytes
SELFDESTRUCT: 0 DELEGATECALL: 0 CREATE2: 0 CALLCODE: 0
CALL: 7 (全部指向 PoolManager 或 SatoToken)
STATICCALL: 1
- 0 DELEGATECALL → 无任何升级/代理跳转的可能;
- 0 SELFDESTRUCT → 合约不可销毁;
- 0 CREATE2 → 不会孵化子合约。
SatoToken 与 SatoHook 链上读取的 GENESIS_BLOCK = 25,015,094 与 GENESIS_HASH = 0xdbb34f…79f4 完全一致——证明两者在同一区块原子部署,没有"先部署一个空壳再替换 minter"的可能性。
部署使用 Arachnid 的 deterministic CREATE2 deployer (0x4e59b4…956C),hook 地址前缀 0x0000… 是为了把 V4 hook permission flag 编码进低 14 位(V4 协议要求),不是单纯的 vanity。
最薄的一层:~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 提款;
unlockCallback用if (msg.sender != address(manager)) revert NotManager()守住;- 字节码:0 DELEGATECALL / 0 SELFDESTRUCT / 0 CREATE2;
- 当前余额 0.064 ETH 是历史 dust,无人能取出(但也无人能多放进去)。
由于
swapper由 router 调用方自由指定(写进 hookData 用作 cooldown 标记),用户可以让别人替自己代付 gas 买入。这是设计选择,不是漏洞。
| 指标 | 值 | 含义 |
|---|---|---|
| 区块高度 | ~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 分钟)
| 风险类别 | 想象的攻击路径 | 实际堵死方式 | 验证手段 |
|---|---|---|---|
| 增发 | 项目方调 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 扫描 + 源码追踪 |
✅ 不属于权限风险,但仍需自行评估:
- 曲线数学正确性:
Curve.sol(mintFor/burnFor)的实现没有审计;如果反向曲线在边界情况(极小或极大 satoIn)下计算错误,可能导致ethCum漂移。 - 市场风险:MAX_BUY_WEI = 5 ETH 的限制让大户必须分多笔吃单,但不能阻止聚合交易者持续砸盘。
- 熵机制溯源:熵窗口已结束(部署后 100 块内),但当时熵奖励基于
blockhash(block.number-1),可被矿工/builder 操纵(窗口已过去,影响已固化)。 - PoolManager 信任:依赖 Uniswap V4 官方 PoolManager 的安全性(业界基础设施级别)。
- 手续费永久锁定:11.7 ETH 已经永远死在 hook 里,未来还会持续累积。这是设计选择("焚毁手续费"),不是 bug,但用户需要知道这部分价值不会回流给任何人。
philosophy()是项目方的 manifesto 字符串:未在本报告中解码,可独立eth_call0x2d37adac 查看,不影响安全性结论。
Sato 协议在权限维度已经做到了一个 ERC‑20 + Uniswap V4 Hook 组合在结构上能达到的最强放弃形态。
部署者保留的唯一一次性权限(
setMinter)已在 block 25,015,096 被使用并锁死;其后没有任何人(包括部署者本人)能修改代币行为、暂停交易、撤池、提取手续费或升级合约。系统由代码完全控制,且代码不可变。剩余风险全部是业务逻辑风险而非信任风险。
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 片段| 时间 (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 已验证发布版本。