Summary
In the natspec of UpliftOnlyExample.sol, it is stated that users are restricted to 100 deposits to avoid DoS issues. On the contrary, users can make more than 100 deposits breaking this invariant.
Vulnerability Details
The protocol's intention to restrict user deposits to 100 is mentioned here.
However, the logic used in UpliftOnlyExample::addLiquidityProportional to ensure this invariant is not broken is flawed. The vulnerability lies in the fact that the function checks if array length is greater than 100 allowing a user to make one deposit more after making 100 deposits.
The affected code is shown in the snippet below and can be found here
function addLiquidityProportional(
address pool,
uint256[] memory maxAmountsIn,
uint256 exactBptAmountOut,
bool wethIsEth,
bytes memory userData
) external payable saveSender(msg.sender) returns (uint256[] memory amountsIn) {
@> if (poolsFeeData[pool][msg.sender].length > 100) {
revert TooManyDeposits(pool, msg.sender);
}
amountsIn = _addLiquidityProportional(
pool,
msg.sender,
address(this),
maxAmountsIn,
exactBptAmountOut,
wethIsEth,
userData
);
uint256 tokenID = lpNFT.mint(msg.sender);
uint256 depositValue = getPoolLPTokenValue(
IUpdateWeightRunner(_updateWeightRunner).getData(pool),
pool,
MULDIRECTION.MULDOWN
);
poolsFeeData[pool][msg.sender].push(
FeeData({
tokenID: tokenID,
amount: exactBptAmountOut,
lpTokenDepositValue: depositValue,
blockTimestampDeposit: uint40(block.timestamp),
upliftFeeBps: upliftFeeBps
})
);
nftPool[tokenID] = pool;
}
Impact
Users can make more than 100 deposits contrary to protocol design thereby, breaking a protocol invariant.
Tools Used
Manual Review
Foundry
Proof of Concept:
A user bob attempts to make 101 deposits
bob makes all 101 deposits successfully showing that a protocol invariant is broken.
PoC
Place the following code into `UpliftExample.t.sol`.
function test_SpomariaPoC_UserCanExceed100AddLiquidity() public {
BaseVaultTest.Balances memory balancesBefore = getBalances(bob);
uint256 numOfDeposits = 101;
uint256[] memory maxAmountsIn = [dai.balanceOf(bob)/numOfDeposits, usdc.balanceOf(bob)/numOfDeposits].toMemoryArray();
vm.prank(bob);
uint256[] memory amountsInFirst = upliftOnlyRouter.addLiquidityProportional(
pool,
maxAmountsIn,
bptAmount / 2,
false,
bytes("")
);
vm.stopPrank();
for(uint i = 1; i < numOfDeposits; i++){
int256[] memory prices = new int256[]();
for (uint256 i = 0; i < 2; ++i) {
prices[i] = (int256(i) * 1e18) / 2;
}
updateWeightRunner.setMockPrices(pool, prices);
skip(5 days);
vm.prank(bob);
upliftOnlyRouter.addLiquidityProportional(
pool,
maxAmountsIn,
bptAmount / 2,
false,
bytes("")
);
vm.stopPrank();
}
assertEq(upliftOnlyRouter.getUserPoolFeeData(pool, bob).length, numOfDeposits, "deposit length incorrect");
}
Recommendations
Consider modifying UpliftOnlyExample::addLiquidityProportional as given below to ensure users are restricted to 100 deposits and thereby preserve the invariant
function addLiquidityProportional(
address pool,
uint256[] memory maxAmountsIn,
uint256 exactBptAmountOut,
bool wethIsEth,
bytes memory userData
) external payable saveSender(msg.sender) returns (uint256[] memory amountsIn) {
- if (poolsFeeData[pool][msg.sender].length > 100) {
+ if (poolsFeeData[pool][msg.sender].length >= 100) {
revert TooManyDeposits(pool, msg.sender);
}
// Do addLiquidity operation - BPT is minted to this contract.
amountsIn = _addLiquidityProportional(
pool,
msg.sender,
address(this),
maxAmountsIn,
exactBptAmountOut,
wethIsEth,
userData
);
uint256 tokenID = lpNFT.mint(msg.sender);
//this requires the pool to be registered with the QuantAMM update weight runner
//as well as approved with oracles that provide the prices
uint256 depositValue = getPoolLPTokenValue(
IUpdateWeightRunner(_updateWeightRunner).getData(pool),
pool,
MULDIRECTION.MULDOWN
);
poolsFeeData[pool][msg.sender].push(
FeeData({
tokenID: tokenID,
amount: exactBptAmountOut,
//this rounding favours the LP
lpTokenDepositValue: depositValue,
//known use of timestamp, caveats are known.
blockTimestampDeposit: uint40(block.timestamp),
upliftFeeBps: upliftFeeBps
})
);
nftPool[tokenID] = pool;
}