sherlock-2025-04-burve-h05
[H-05] Attacker captures unclaimed fees by timing deposit with range re-entry and price manipulation
Summary
UniV3 풀에서 발생한 수수료를 UniV3 풀에 재예치하는데, 예치 비율에 맞지 않는 토큰은 컨트랙트에 남겨두고 다음 재예치에 사용했다. 이로 인해 남아있던 수수료를 가져갈 자격이 없는 신규 유저가 수수료를 가져가거나, 기존 유저가 받아야 하는 수수료를 받지 못하는 상황이 발생한다.
Keyword
interest, crypto theft, logic flaw
Vulnerability
Single 풀에 유저가 입금을 하거나 출금을 할 때마다 UniV3 풀의 수수료를 수집하고, 이를 다시 각 UniV3 범위에 비율에 따라 재예치한다. 재예치된 수수료는 유저가 Single 풀에서 출금을 할 때 꺼내져 유저에게 수익을 준다.
수수료 수수료를 풀에 재분배할 때, 컨트랙트에 남아있는 토큰의 비율이 유동성 풀이 요구하는 비율과 다르다면 양이 작은 양을 가진 토큰을 기준으로 예치할 유동성의 양을 정한다. 예를 들어 컨트랙트에 token0 1000개, token1 1200개, 유동성 제공 비율이 1:1인 경우, 프로토콜은 token0 1000개와 token1 1000개를 사용하여 유동성을 제공하고 나머지 token1은 200개는 컨트랙트에 남아있는다.
function collectAndCalcCompound()
internal
returns (uint128 mintNominalLiq)
{
// collected amounts on the contract from: fees, compounded leftovers, or tokens sent to the contract.
uint256 collected0 = token0.balanceOf(address(this));
uint256 collected1 = token1.balanceOf(address(this));
...
// compute liq in collected amounts
(
uint256 amount0InUnitLiqX64,
uint256 amount1InUnitLiqX64
) = getCompoundAmountsPerUnitNominalLiqX64();
@> uint256 nominalLiq0 = amount0InUnitLiqX64 > 0
? (collected0 << 64) / amount0InUnitLiqX64
: uint256(type(uint128).max);
@> uint256 nominalLiq1 = amount1InUnitLiqX64 > 0
? (collected1 << 64) / amount1InUnitLiqX64
: uint256(type(uint128).max);
@> uint256 unsafeNominalLiq = nominalLiq0 < nominalLiq1
? nominalLiq0
: nominalLiq1;
// We should never be able to compound infinite liquidity into both tokens at once, either
// 1) the contract was misconfigured and only consists of a single island or
// 2) there is something seriously broken with the underlying v3 pool
// In either case this event serves as a warning.
// We don't revert because that would block calls to mint / burn.
if (unsafeNominalLiq == uint256(type(uint128).max)) {
emit MalformedPool();
}
// min calculated liquidity with the max allowed
mintNominalLiq = unsafeNominalLiq > type(uint128).max
? type(uint128).max
: uint128(unsafeNominalLiq);
// during mint the liq at each range is rounded up
// we subtract by the number of ranges to ensure we have enough liq
mintNominalLiq = mintNominalLiq <= (2 * distX96.length)
? 0
: mintNominalLiq - uint128(2 * distX96.length);
}다음 유저는 다음 수수료 재예치에서 남아있는 token1 200개를 사용할 수 있다. 이로 인해 여러 문제가 발생할 수 있다. 다음 시나리오를 생각해보자.
token0: USDC, token1: USDT
Alice는 10,000 USDC와 10,000 USDT를 예치하고 10,000 share를 얻는다. 이후 90 USDC의 수수료를 UniV3 풀에서 받는다.
- Case1
- 이 상황에서 Alice가 10,000 share 를 소각하면 UniV3에서 발생한 수수료를 전혀 받지 못하고 10,000 USDC와 10,000 USDT만 돌려받는다.
- Case2
- Bob이 20,000 USDC와 20,000 USDT를 예치하고 20,000 share를 받는다. Bob이 90 USDT를 프로토콜에 기부한다.
- Bob이 20,000 share를 소각하면 먼저 90 USDC와 90 USDT가 수수료로 풀에 입금된다.
- 이후 출금 처리로 인해 Bob은 20,060 USDC와 20,060 USDT 총 40,120 USD를 받을 수 있다.
- 이 수수료는 Bob이 입금하기 전에 발생한 것으로 모두 Alice의 몫이었다. 하지만 유동성 비율이 맞지 않아 입금되지 못하였다.
- 결과적으로 Bob은 프로토콜에서 30 USD를 훔치는 효과가 있다.
Impact
유저가 수수료를 가져갈 수 없다. 공격자가 이 취약점을 악용하여 보상을 빼돌릴 수 있다.
Mitigation
입출금 시 유동성에 예치되어 있지 않은 컨트랙트에 남아있는 토큰도 비율에 따라 분배한다.
tags: bughunting, burve, smart contract, solidity, crypto theft, logic flaw, severity high