QuantAMM

QuantAMM
49,600 OP
View results
Submission Details
Severity: medium
Invalid

Missing Hook Implementation Prevents Liquidity Removal

Summary

The UpliftOnlyExample hook contract fails to properly implement the onBeforeRemoveLiquidity function, causing all liquidity removal operations to revert when shouldCallBeforeRemoveLiquidity is enabled. This creates a denial of service condition where users cannot remove their liquidity from affected pools.

Vulnerability Details

In the Vault.sol contract, when removing liquidity, the vault checks if shouldCallBeforeRemoveLiquidity() is true. If enabled, it calls the hook's callBeforeRemoveLiquidityHook function which returns false by default since onBeforeRemoveLiquidity is not implemented in the UpliftOnlyExample contract.

This differs from liquidity addition where the hook's onBeforeAddLiquidity function is properly implemented to return true, allowing the operation to proceed.

The relevant code here shows that for removing liquidity it will revert since there is no override of the onBeforeRemoveLiquidity function.

if (poolData.poolConfigBits.shouldCallBeforeRemoveLiquidity()) {
HooksConfigLib.callBeforeRemoveLiquidityHook(
minAmountsOutScaled18,
msg.sender,
params,
poolData,
_hooksContracts[params.pool]
);
}
// callBeforeRemoveLiquidityHook
function callBeforeRemoveLiquidityHook(
uint256[] memory minAmountsOutScaled18,
address router,
RemoveLiquidityParams memory params,
PoolData memory poolData,
IHooks hooksContract
) internal {
if (
hooksContract.onBeforeRemoveLiquidity(
router,
params.pool,
params.kind,
params.maxBptAmountIn,
minAmountsOutScaled18,
poolData.balancesLiveScaled18,
params.userData
) == false
) {
revert IVaultErrors.BeforeRemoveLiquidityHookFailed();
}
}
// onBeforeRemoveLiquidity
/// @inheritdoc IHooks
function onBeforeRemoveLiquidity(
address,
address,
RemoveLiquidityKind,
uint256,
uint256[] memory,
uint256[] memory,
bytes memory
) public virtual returns (bool) {
return false;
}

But when adding liquidity it is overriding the default function

if (poolData.poolConfigBits.shouldCallAfterAddLiquidity()) {
// `hooksContract` needed to fix stack too deep.
IHooks hooksContract = _hooksContracts[params.pool];
amountsIn = poolData.poolConfigBits.callAfterAddLiquidityHook(
msg.sender,
amountsInScaled18,
amountsIn,
bptAmountOut,
params,
poolData,
hooksContract
);
}
// callAfterAddLiquidityHook
function callAfterAddLiquidityHook(
PoolConfigBits config,
address router,
uint256[] memory amountsInScaled18,
uint256[] memory amountsInRaw,
uint256 bptAmountOut,
AddLiquidityParams memory params,
PoolData memory poolData,
IHooks hooksContract
) internal returns (uint256[] memory) {
(bool success, uint256[] memory hookAdjustedAmountsInRaw) = hooksContract.onAfterAddLiquidity(
router,
params.pool,
params.kind,
amountsInScaled18,
amountsInRaw,
bptAmountOut,
poolData.balancesLiveScaled18,
params.userData
);
// ... existing code ...
}
// onAfterAddLiquidity
function onAfterAddLiquidity(
address,
address pool,
AddLiquidityKind kind,
uint256[] memory,
uint256[] memory amountsInRaw,
uint256,
uint256[] memory,
bytes memory
) public override onlyVault returns (bool success, uint256[] memory) {
// Our current architecture only supports fees on tokens. Since we must always respect exact `amountsIn`, and
// non-proportional add liquidity operations would require taking fees in BPT, we only support proportional
// addLiquidity.
if (kind != AddLiquidityKind.PROPORTIONAL) {
// Returning false will make the transaction revert, so the second argument does not matter.
return (false, amountsInRaw);
}
IERC20[] memory tokens = _vault.getPoolTokens(pool);
uint256[] memory hookAdjustedAmountsInRaw = amountsInRaw;
if (addLiquidityHookFeePercentage > 0) {
// Charge fees proportional to amounts in of each token.
for (uint256 i = 0; i < amountsInRaw.length; i++) {
uint256 hookFee = amountsInRaw[i].mulUp(addLiquidityHookFeePercentage);
if (hookFee > 0) {
hookAdjustedAmountsInRaw[i] += hookFee;
// Sends the hook fee to the hook and registers the debt in the Vault.
_vault.sendTo(tokens[i], address(this), hookFee);
emit HookFeeCharged(address(this), tokens[i], hookFee);
}
}
}
return (true, hookAdjustedAmountsInRaw);
}

The UpliftOnlyExample contract inherits from BaseHooks but does not override the onBeforeRemoveLiquidity function, causing it to return false by default and making all liquidity removal operations revert.

Impact

DOS - Users are unable to remove liquidity from pools that have shouldCallBeforeRemoveLiquidity enabled, effectively locking their funds in the pool.

Tools Used

Manual Review

Recommendations

Override the onBeforeRemoveLiquidity function to return true so that operations can proceed. Additionally consider overide all relevant functions that would lead to a revert in execution and have them return true so that execution can proceed

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.