`AutoExit::execute()` function allows an attacker to drain the liquidity of a Uniswap v3 pool by repeatedly calling the function with the same `tokenId` and `liquidity` values #483
AutoExit::execute() function allows an attacker to drain the liquidity of a Uniswap v3 pool by repeatedly calling the function with the same tokenId and liquidity values, while manipulating the tick value of the pool.
The bug is caused by the fact that the function does not properly check whether the tick value of the pool has changed since the last call to execute(), allowing an attacker to repeatedly execute the function with the same tokenId and liquidity values, while manipulating the tick value of the pool.
Proof of Concept
Create a Uniswap v3 pool with a small amount of liquidity.
Approve the AutoExit contract to spend the liquidity tokens.
Configure the AutoExit contract with a tokenId and liquidity value corresponding to the Uniswap v3 pool.
Call the execute() function with a high rewardX64 value and a tokenId and liquidity value corresponding to the Uniswap v3 pool.
Manipulate the tick value of the pool to trigger the isSwap condition in the execute() function.
Repeat steps 4 and 5 until the liquidity of the pool is drained.
Tools Used
Manual Review
Recommended Mitigation Steps
The execute() function should include a check to ensure that the tick value of the pool has changed since the last call to execute(). This can be done by storing the tick value in a mapping that maps tokenId to tick values, and checking the stored tick value before executing any swap or remove operations. The function should also include a check to ensure that the liquidity value has not changed since the last call to execute().
Here is an example of how the execute() function can be modified to include these checks:
function execute(ExecuteParams calldata params) external {
if (!operators[msg.sender]) {
revert Unauthorized();
}
ExecuteState memory state;
PositionConfig memory config = positionConfigs[params.tokenId];
if (!config.isActive) {
revert NotConfigured();
}
if (
config.onlyFees && params.rewardX64 > config.maxRewardX64
|| !config.onlyFees && params.rewardX64 > config.maxRewardX64
) {
revert ExceedsMaxReward();
}
// get position info
(,, state.token0, state.token1, state.fee, state.tickLower, state.tickUpper, state.liquidity,,,,) =
nonfungiblePositionManager.positions(params.tokenId);
// so can be executed only once
if (state.liquidity == 0) {
revert NoLiquidity();
}
if (state.liquidity != params.liquidity) {
revert LiquidityChanged();
}
state.pool = _getPool(state.token0, state.token1, state.fee);
(, state.tick,,,,,) = state.pool.slot0();
// check if tick has changed since last call to execute()
if (lastTicks[params.tokenId] == state.tick) {
revert TickUnchanged();
}
lastTicks[params.tokenId] = state.tick;
// not triggered
if (config.token0TriggerTick <= state.tick && state.tick < config.token1TriggerTick) {
revert NotReady();
}
state.isAbove = state.tick >= config.token1TriggerTick;
state.isSwap = !state.isAbove && config.token0Swap || state.isAbove && config.token1Swap;
// decrease full liquidity for given position - and return fees as well
(state.amount0, state.amount1, state.feeAmount0, state.feeAmount1) = _decreaseFullLiquidityAndCollect(
params.tokenId, state.liquidity, params.amountRemoveMin0, params.amountRemoveMin1, params.deadline
);
// swap to other token
if (state.isSwap) {
if (params.swapData.length == 0) {
revert MissingSwapData();
}
// reward is taken before swap - if from fees only
if (config.onlyFees) {
state.amount0 -= state.feeAmount0 * params.rewardX64 / Q64;
state.amount1 -= state.feeAmount1 * params.rewardX6
Lines of code
https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/automators/AutoExit.sol#L100-L215
Vulnerability details
Impact
AutoExit::execute()
function allows an attacker to drain the liquidity of a Uniswap v3 pool by repeatedly calling the function with the sametokenId
andliquidity
values, while manipulating thetick
value of the pool.The bug is caused by the fact that the function does not properly check whether the
tick
value of the pool has changed since the last call toexecute()
, allowing an attacker to repeatedly execute the function with the sametokenId
andliquidity
values, while manipulating thetick
value of the pool.Proof of Concept
tokenId
andliquidity
value corresponding to the Uniswap v3 pool.execute()
function with a highrewardX64
value and atokenId
andliquidity
value corresponding to the Uniswap v3 pool.tick
value of the pool to trigger theisSwap
condition in theexecute()
function.Tools Used
Manual Review
Recommended Mitigation Steps
The
execute()
function should include a check to ensure that thetick
value of the pool has changed since the last call toexecute()
. This can be done by storing thetick
value in a mapping that mapstokenId
totick
values, and checking the storedtick
value before executing any swap or remove operations. The function should also include a check to ensure that theliquidity
value has not changed since the last call toexecute()
.Here is an example of how the
execute()
function can be modified to include these checks:Assessed type
Uniswap