Closed howlbot-integration[bot] closed 4 months ago
alex-ppg marked the issue as unsatisfactory: Invalid
Hey there @alex-ppg
I'd like to invite you to reconsider this issue with sqrtPriceX96
manipulation again.
You disputed #65 and #53 with the reason that price manipulation is impossible because no one has SALE_TOKEN
before launch. However, I've shown here in this finding (and also from another Warden, #41) that it is possible to manipulate sqrtPriceX96
in an empty pool. Adversaries do not have to hold any SALE_TOKEN
to perform this attack.
I'm the author of #41, and I agree with the comments of @nnez here. I invite the judge to also look at my issue for further insights.
I think these two issues have been invalidated due to the big amount of invalid/qa issues with dup-65.
Many of those issues make invalid claims, assuming that there will be liquidity in the pool.
Others just refer to modifying the sqrtPriceX96
before the launch, which is possible by "swapping" zero tokens when there is no liquidity, but this can be remediated by "swapping" back to the original price, and the pool will still launch successfully. I showcased this as a bonus on my unsuccessfulPriceManipulation()
test. This could be considered (or not) qa as a separate issue.
As they don't get the actual root cause, their recommendations are also invalid.
The actual attack vector and root cause is only mentioned on this issue and on issue #41.
The root cause is the possibility to mint single-sided liquidity after performing the swap.
Swapping the token to alter the price is not sufficient, as it can easily be set back to its expected value as explained before and shown in my test.
By minting single-side liquidity, the price can get back to its expected value, satisfying sqrtPriceX96Existing == sqrtPriceX96
, but the pool launch will still fail, as not enough liquidity can be minted due to the attack. This can't be remediated, and a new vesting offering will have to be created.
Both reports show POCs to prove it.
Regarding severity, I'd like to make a case for it being High. I explained the reasoning on the "Impact" section of my report.
The key points are that it can be done at any time by an attacker with almost no cost. It could be done just a minute before the expected pool launch, after raising capital from many investors during multiple days.
A failing launch under these circumstances will clearly have an impact on the appreciation of the token price for a future raise of investment, as this token launch will fail as explained in the reports. Additionally, users would have to spend gas on mainnet to recover their failed investment.
Also worth mentioning that not only the Vultisig coin launch would be affected, but also all other projects that intend to use the platform for raising capital.
That said, I'd like to ask to only consider these issues related to minting single-side liquidity on their own as High.
Other issues might be qa/invalid, depending on their claims, considering that just altering the sqrtPriceX96
before the launch can be easily remediated as shown on my POC on #41.
hi @alex-ppg I agreed with comment above. Issue #65 and #53 is not actually show out the root cause and mintigation step are invalid. Issue #41 point out exactly issue and have a valid POC
@Haupc Sponsors are not allowed to close, reopen, or assign issues or pull requests.
Thank you @nnez, @0xJuancito, and @Haupc for your PJQA contributions! This is indeed a case of a miscreated duplicate group that had an actual vulnerability described within it.
I have proceeded to separate this as a new issue in the repository given that #41 was deemed as the "primary" submission, #179 was deemed to be of a partial-75
reward, and #77 was deemed to be of a partial-25
reward.
alex-ppg marked the issue as not a duplicate
alex-ppg marked the issue as duplicate of #41
Hey @alex-ppg
Thanks for looking into this. I'm just curious here though.
Should the unsatistactory
label be changed to sastisfactory
?
alex-ppg marked the issue as satisfactory
alex-ppg changed the severity to 3 (High Risk)
There are still some more cleanup grades to assign @nnez as I am waiting on 10 findings being migrated over from the findings repository, but thank you for the reminder!
Lines of code
https://github.com/code-423n4/2024-06-vultisig/blob/main/src/ILOManager.sol#L187-L198
Vulnerability details
Impact
Description
Project's admin specify an initial pool price when initiate a new project.
When all conditions in all pools are met, anyone can call
launch
function inILOManager.sol
to launch their ILO.Inside
launch
function, there is a check that current pool'ssqrtPriceX96
is equal to the initial pool price set during project initiation.The check ensures that ILO launches at the project specified price.
See: ILOManager.sol#L187-L198
Manipulate sqrtPriceX96 of UniswapV3 pool
UniswapV3 pool stores
sqrtPriceX96
inslot0
. It is the current price corresponding with the current tick of the pool.It changes when the swap operation cross the tick.
However, in an empty pool (liquidity=0), after pool's initialization with
sqrtPriceX96
, UniswapV3 pool behaves weirdly.Calling
swap
function on the pool will not revert.swap(address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data)
In turn, the pool will cross to the corresponding tick of the specified
sqrtPriceLimitX96
, requiring no tokens transfer.Single-sided liquidity
Providing liquidity in Uniswap V3 pool outside of current price/tick requires only single token of the pair.
Reference: Single-sided Liquidity
Attack scenario
Let's say Project A wants to launch a new ILO for their new token, $NEW and pair it with $USDC. They proceed to init their project and various pools.
Supposed that the newly deployed UniswapV3 pool from
initProject
hastoken0=$USDC
andtoken1=$NEW
and they choosek
as initial price, equals to tick=t_k
.Adversaries can manipulate
sqrtPriceX96
(lower it down) to a valuex; x < k
, by callingswap
function directly as mentioned in the previous section. Then, provide single-sided liquidity of token0 (USDC) by specifying the tick range just between the pricex
andk
.Then, when
block.timestamp
reaches launch time, pool's admin will not be able to calllaunch
function because the current spot price is now different from the initial pool price.Whether the launch is recoverable depends on certain factors:
sqrtPriceX96
back using the same method as adversaries.x
andk
because pool's owner have to use their ownSALE_TOKEN
to manipulate the price back to intended price.SALE_TOKEN
to ILOPool contract andSALE_TOKEN
is a fixed supply. Then, the launch is unrecoverable.Proof-of-Concept
The test below does the following:
sqrtPriceX96
and provide single-sided liquiditySALE_TOKEN
to revertsqrtPriceX96
backSteps to reproduce
2024-06-vultisig/test/ILOPool.man.t.sol
with provide code belowforge test --match-contract ILOPreventLaunchTest --match-test testPreventLaunch -vv
pragma solidity =0.7.6; pragma abicoder v2;
import '../src/interfaces/IILOPool.sol'; import '../src/interfaces/IILOWhitelist.sol'; import '../src/interfaces/IILOVest.sol'; import "../src/ILOManager.sol"; import "../src/ILOPool.sol"; import "./IntegrationTestBase.sol"; import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import {TickMath} from "@uniswap/v3-core/contracts/libraries/TickMath.sol";
contract UniswapCallback{
}
contract ILOPreventLaunchTest is Test {
}
Running 1 test for test/ILOPool.man.t.sol:ILOPreventLaunchTest [PASS] testPreventLaunch() (gas: 9521713) Logs: @> Init project, initialPoolPriceX96=158456325028528675187087900672 @> ProjectId (univ3pool): 0x8005a9E9643F2e5E165c67a5162c5169C278B7b4 @> Init pool @> Whitelist investor (0x958A7E75e1a51269bd267D873357909d17623971) @> Simulate buy transaction buy INVESTOR @> Prepare launch @>> Put SALE_TOKEN in ILOPool and warp to launch time @> Save snapshot before attack @> Commence attack @> Query pool slot0 @>> slot0.sqrtPriceX96: 158456325028528675187087900672 @>> slot0.tick: 13863 @> Call swap to manipulate sqrtPriceX96 @> Provide single-sided liquidity to complicate recovery @> Query pool slot0 @>> slot0.sqrtPriceX96: 43482641866909686834497309393 @>> slot0.tick: -12000 @> Done attack @> Project's admin try to call launch, expecting revert @> Revert with 'UV3P' @> Project's admin try to recover launch @> SALE_TOKEN balnce of admin before: 1e22 @> Call swap to manipulate sqrtPriceX96 back @> SALE_TOKEN balnce of admin after: 9.999999999999900521672e21 @> Query pool slot0 @>> slot0.sqrtPriceX96: 158456325028528675187087900672 @>> slot0.tick: 13863 @> Project's admin try to launch again, should pass @> Recover vm state to before attack @> Query pool slot0 @>> slot0.sqrtPriceX96: 158456325028528675187087900672 @>> slot0.tick: 13863 @> Launch again, should pass
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 18.31s
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)