Platform: Code4rena
Start Date: 21/12/2021
Pot Size: $30,000 USDC
Total HM: 20
Participants: 20
Period: 5 days
Judge: Jack the Pug
Total Solo HM: 13
Id: 70
League: ETH
Rank: 5/20
Findings: 3
Award: $1,456.80
🌟 Selected for report: 2
🚀 Solo Findings: 0
🌟 Selected for report: TomFrenchBlockchain
Also found by: hyh
647.066 USDC - $647.07
hyh
Pool funds will be siphoned out over time as swaps and asymmetric LP provision are generally balancing each other economically. While with introduction of IL reimbursement a malicious user can make an asymmetric LP, then profit immediately from out of balance pool with a swap and profit again after a while from IL coverage. This requires locking liquidity to a pool to become covered by IL protection, but still represents an additional profit without additional risk at expense of reserve funds.
Another variant of exploiting this is to add liquidity in two steps: deposit #1 with zero slip adjustment, perfectly matching current market price, deposit #2 with more Vader than market price suggests, moving pool out of balance with Vader becoming cheaper, then exiting deposit #1 with profit because slip adjustment reduced deposit #2 related share issuance and deposit #1 now has more asset claims than before. Deposit #2 then need to wait and exit after some time to receive IL protection coverage:
IL is calculated as ((originalAsset * releasedVader) / releasedAsset) + originalVader - ((releasedAsset * releasedVader) / releasedAsset) + releasedVader
, i.e. original deposit values without taking account of slip adjustment are used, so providing more Vader in deposit #2 leads to greater IL, which this way have two parts: market movements related and skewed liquidity provision related. IL covering compensates for slip adjustments this way, which is exploitable at scale as long as asymmetric IL is allowed.
The steps to reproduce are:
BasePoolV2 NFT mint: https://github.com/code-423n4/2021-12-vader/blob/main/contracts/dex-v2/pool/BasePoolV2.sol#L479
VaderPoolV2 NFT burn: https://github.com/code-423n4/2021-12-vader/blob/main/contracts/dex-v2/pool/VaderPoolV2.sol#L265 IL coverage there: https://github.com/code-423n4/2021-12-vader/blob/main/contracts/dex-v2/pool/VaderPoolV2.sol#L293
IL calculation: https://github.com/code-423n4/2021-12-vader/blob/main/contracts/dex/math/VaderMath.sol#L72
Our view is that asymmetric liquidity provision doesn't provide much business value, introducing substantial attack surface, so the core recommendation here is to remove a possibility to add liquidity asymmetrically.
For example, the following can be implemented: instead of penalizing LP with slip adjustment do the biggest liquidity additions with zero slip adjustment that user provided funds allow, and then transfer the remaining part of the funds to a user.
Allowing only symmetric liquidity addition completely removes the described attack surface.
#0 - jack-the-pug
2022-03-13T15:57:38Z
Dup #55
647.066 USDC - $647.07
hyh
Users that mint synths do provide native assets, increasing native reserve pool, but do not get any liquidity shares issued. In the same time, an exit of non-synth liquidity provider yields releasing a proportion of all current reserves to him.
Whenever an exit of non-synth LP is substantial enough, the system will have much less native asset regarding the cumulative deposit of synth holders. That is, when a LP entered he provided a share of current reserves, both native and foreign, and got the corresponding liquidity shares in return. Suppose then big enough amounts of synths were minted, providing correspondingly big enough amount of native assets. If the LP now wants to exit, he will obtain a part of total native assets, including a part of the amount that was provided by synth minter. If the exit is big enough there will be substantially less native assets left to reimburse the synth minter than he initially provided. This is not reversible: the synth minters lost their native assets to LP that exited.
There are three types of mint/burn: NFT, fungible and synths. First two get LP shares, the latter gets synths. Whenever NFT or fungible LP exits, it gets a proportion of combined reserves. That is, some of native reserves were deposited by synth minters, but it is not accounted anyhow, only one total reserve number per asset is used.
Suppose the following scenario, Alice wants to provide liquidity, while Bob wants to mint synths:
The issue is that synth provided and LP provided USDV aren't accounted separately, total reserves number if used everywhere instead:
Synth minters provide native asset, say USDV, to the system: https://github.com/code-423n4/2021-12-vader/blob/main/contracts/dex-v2/pool/VaderPoolV2.sol#L187
Synth minters get synths and no LP shares, while to account for their deposit, the total USDV balance is increased: https://github.com/code-423n4/2021-12-vader/blob/main/contracts/dex-v2/pool/VaderPoolV2.sol#L187
When LP enters, it gets liquidity shares proportionally to current reserves (NFT mint, notice the reserveNative, which is BasePoolV2's pair.reserveNative, total amount of native asset in the Pool): https://github.com/code-423n4/2021-12-vader/blob/main/contracts/dex-v2/pool/BasePoolV2.sol#L497
When LP exits, it gets a proportion of current reserves back (NFT burn): https://github.com/code-423n4/2021-12-vader/blob/main/contracts/dex-v2/pool/BasePoolV2.sol#L223
The same happens when fungible LP mints (same reserveNative): https://github.com/code-423n4/2021-12-vader/blob/main/contracts/dex-v2/pool/VaderPoolV2.sol#L336 And burns: https://github.com/code-423n4/2021-12-vader/blob/main/contracts/dex-v2/pool/VaderPoolV2.sol#L401
Account for LP provided liquidity separately from total amount variables, i.e. use only LP provided native reserves variables in LP shares mint and burn calculations. That should suffice as total amount of native assets is still to be used elsewhere, whenever the whole pool is concerned, for example, in rescue function, swap calculations and so forth.
🌟 Selected for report: defsec
Also found by: Jujic, TomFrenchBlockchain, danb, hyh
18.8684 USDC - $18.87
hyh
If Chainlink latestRoundData
for any reason will return a most recent round that wasn't updated yet, but have some positive price, it will be used as current price despite there will be no confirmation for it.
In LiquidityBasedTWAP Chainlink's latestRoundData
isn't checked for non-zero round update timestamp, which is discarded, and any positive price with roundID == answeredInRound
is accepted as current:
https://github.com/code-423n4/2021-12-vader/blob/main/contracts/lbt/LiquidityBasedTWAP.sol#L85
Chainlink documentation warns that recent round is not guaranteed to be complete and advices to validate for non-zero timestamp as a general approach: https://docs.chain.link/docs/historical-price-data/#historical-rounds
latestRoundData
returns updated at
timestamp in updatedAt
field:
https://docs.chain.link/docs/feed-registry/#latestrounddata
Require that the round used for price discovery was submitted, i. e. that its updatedAt
timestamp isn't zero.
Now:
function getChainlinkPrice(address asset) public view returns (uint256) { IAggregatorV3 oracle = oracles[asset]; (uint80 roundID, int256 price, , , uint80 answeredInRound) = oracle.latestRoundData(); require( answeredInRound >= roundID, "LBTWAP::getChainlinkPrice: Stale Chainlink Price" );
To be:
function getChainlinkPrice(address asset) public view returns (uint256) { IAggregatorV3 oracle = oracles[asset]; (uint80 roundID, int256 price, , uint256 updatedAt, uint80 answeredInRound) = oracle.latestRoundData(); require( updatedAt != 0 && answeredInRound >= roundID, "LBTWAP::getChainlinkPrice: Stale Chainlink Price" );
#0 - SamSteinGG
2021-12-27T10:30:57Z
The link referenced does not apply to the latest round data function.
#1 - jack-the-pug
2022-03-12T15:10:46Z
Dup #111
🌟 Selected for report: hyh
143.7924 USDC - $143.79
hyh
BasePoolV2 has gas validation modifiers used in its functions, while VaderPoolV2 doesn't yet. As setting management is implemented in VaderPoolV2, but not in BasePoolV2, other BasePoolV2 descendants will have to reimplement this function for no reason as there are no specifics there and, as general purpose functionality, it should be implemented in BasePoolV2.
All gas validation is used in base pool functions only: https://github.com/code-423n4/2021-12-vader/search?q=validateGas
However, gasThrottle setting management is implemented in VaderPoolV2, despite being of general purpose: https://github.com/code-423n4/2021-12-vader/blob/main/contracts/dex-v2/pool/VaderPoolV2.sol#L479
Move setGasThrottle implementation from VaderPoolV2 to BasePoolV2.