Platform: Code4rena
Start Date: 13/03/2023
Pot Size: $72,500 USDC
Total HM: 33
Participants: 35
Period: 7 days
Judge: Dravee
Total Solo HM: 16
Id: 222
League: ETH
Rank: 10/35
Findings: 5
Award: $1,440.31
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: joestakey
Also found by: 0x52, Bauer, KIntern_NA, auditor0517, bin2chen, chaduke, juancito
79.8459 USDC - $79.85
https://github.com/code-423n4/2023-03-polynomial/blob/aeecafc8aaceab1ebeb94117459946032ccdff1e/src/KangarooVault.sol#L424-L426 https://github.com/code-423n4/2023-03-polynomial/blob/aeecafc8aaceab1ebeb94117459946032ccdff1e/src/KangarooVault.sol#L436-L447
The owner of the KangarooVault
can't receive collateral from EXCHANGE
when he wants to remove collateral from the vault.
KangarooVault.removeCollateral
doesn't remove collateral from the EXCHANGE
. KangarooVault
interacts with EXCHANGE
, and the owner of KangarooVault
can add/remove collateral to manage short positions.
In the implementation of addCollateral
, it calls addCollateral
of EXCHANGE
as expected.
function addCollateral(uint256 additionalCollateral) external requiresAuth nonReentrant { SUSD.safeApprove(address(EXCHANGE), additionalCollateral); EXCHANGE.addCollateral(positionData.positionId, additionalCollateral);
But in the implementation of removeCollateral
, there is no such thing. KangarooVault.removeCollateral
doesn't interact with EXCHANGE
and only updates states.
function removeCollateral(uint256 collateralToRemove) external requiresAuth nonReentrant { (uint256 markPrice,) = LIQUIDITY_POOL.getMarkPrice(); uint256 minColl = positionData.shortAmount.mulWadDown(markPrice); minColl = minColl.mulWadDown(collRatio); require(positionData.totalCollateral >= minColl + collateralToRemove); usedFunds -= collateralToRemove; positionData.totalCollateral -= collateralToRemove; emit RemoveCollateral(positionData.positionId, collateralToRemove); }
As a result, the owner of the KangarooVault
can't receive collateral from EXCHANGE
when he wants to remove collateral from the vault.
Manual Review
We should call EXCHANGE.removeCollateral
to make sure to receive collaterals.
EXCHANGE.removeCollateral(positionData.positionId, collateralToRemove);
#0 - c4-judge
2023-03-21T00:12:18Z
JustDravee marked the issue as satisfactory
#1 - c4-judge
2023-03-21T00:12:35Z
JustDravee marked the issue as duplicate of #111
#2 - JustDravee
2023-05-02T23:19:54Z
Partial for lack of coded POC on a High
#3 - c4-judge
2023-05-02T23:20:17Z
JustDravee marked the issue as partial-50
🌟 Selected for report: peakbolt
Also found by: 0xRobocop, 0xbepresent, auditor0517, kaden
175.2447 USDC - $175.24
totalFunds
is not updated in Liquidity.openShort
so totalFunds
will be wrong.
Liquidity.openShort
updates usedFunds
only and doesn't update totalFunds
. totalFunds
should be updated after openShort
.
Manual Review
totalFunds
should be updated in openShort
.
totalFunds += feesCollected - externalFee;
#0 - c4-judge
2023-03-21T11:31:11Z
JustDravee marked the issue as duplicate of #153
#1 - c4-judge
2023-03-21T11:34:54Z
JustDravee marked the issue as partial-25
#2 - c4-judge
2023-03-25T04:42:19Z
JustDravee marked the issue as full credit
#3 - JustDravee
2023-05-02T22:42:05Z
Partial for lack of coded POC on a high
#4 - c4-judge
2023-05-02T22:42:25Z
JustDravee marked the issue as partial-50
🌟 Selected for report: peakbolt
Also found by: KIntern_NA, auditor0517
721.1716 USDC - $721.17
https://github.com/code-423n4/2023-03-polynomial/blob/aeecafc8aaceab1ebeb94117459946032ccdff1e/src/LiquidityPool.sol#L472-L484 https://github.com/code-423n4/2023-03-polynomial/blob/aeecafc8aaceab1ebeb94117459946032ccdff1e/src/LiquidityPool.sol#L807-L808 https://github.com/code-423n4/2023-03-polynomial/blob/aeecafc8aaceab1ebeb94117459946032ccdff1e/src/LiquidityPool.sol#L549 https://github.com/code-423n4/2023-03-polynomial/blob/aeecafc8aaceab1ebeb94117459946032ccdff1e/src/LiquidityPool.sol#L516
usedFunds
is wrong in LiquidityPool
, and usedFunds
tracks spent quote tokens. usedFunds
is an important state in LiquidityPool
, so the impact will be high.
Liquidity.closeLong
and openShort
don't update the state usedFunds
correctly.
In the implementation of closeLong
, tradeCost
is added to usedFunds
.
usedFunds += int256(tradeCost);
But tradeCost
already contains hedgingFees
and hedgingFees
are added to usedFunds
in _hedge
method before.
uint256 marginRequired = _calculateMargin(hedgingSize) + hedgingFees; usedFunds += int256(marginRequired);
So hedgingFees
are added to usedFunds
twice, and usedFunds
will be wrong. There are similar things in openShort
method, too. In the implementation of openShort
, hedgingFees
are added to usedFunds
twice from direct addition and _hedge
method similarly.
usedFunds += int256(totalCost + hedgingFees + externalFee);
Manual Review
we can use totalCost
instead of tradeCost
to update usedFunds
as follows for closeLong
. And same thing for openShort
.
usedFunds += int256(totalCost);
And this is for closeShort
:
usedFunds -= int256(tradeCost);
#0 - c4-judge
2023-03-21T11:40:28Z
JustDravee marked the issue as duplicate of #152
#1 - c4-judge
2023-05-02T22:32:54Z
JustDravee marked the issue as satisfactory
🌟 Selected for report: peakbolt
Also found by: auditor0517
360.5858 USDC - $360.59
getTokenPrice
can revert and the pool's deposit and withdraw will not work.
getTokenPrice
returns 1e18 when totalFunds
= 0, but the correct condition is totalSupply
= 0.
getTokenPrice
reverts when totalSupply
= 0 at the following line.
return totalValue.divWadDown(totalSupply);
If totalFunds
is always 0 when totalSupply
= 0, getTokenPrice
will return the correct value of 1e18.
if (totalFunds == 0) { return 1e18; }
But totalFunds
can be a dust value when totalSupply
= 0. This situation can be happen in several conditions, and the correct token price is 1e18 instead of reversion. If getTokenPrice
reverts, there is no way to deposit
and it can result a DOS.
One of the possible senarios is for the first depositor. The first depositor can deposit and withdraw in the same transaction. He will send totalFunds
for the first deposit, and the token price will be totalFunds.divWadDown(totalSupply)
after that. If he withdraws all liquidity tokens (= total supply), susdToReturn = tokens.mulWadDown(tokenPrice)
in withdraw
method.
So if totalSupply
> 1e18, susdToReturn
can be slightly less than totalFunds
, and totalFunds
can be a non-zero dust value. After this, there is no way to deposit/withdraw and this will result DOS.
Similar thing can be happen in KangarooVault
because implementation of getTokenPrice
is very similar.
Manual Review
getTokenPrice
should return 1e18 when totalSupply
= 0.
#0 - c4-judge
2023-03-22T18:53:52Z
JustDravee marked the issue as duplicate of #157
#1 - c4-judge
2023-05-03T00:50:40Z
JustDravee marked the issue as satisfactory
🌟 Selected for report: rbserver
Also found by: 0xSmartContract, DadeKuma, GalloDaSballo, PaludoX0, RaymondFam, Rolezn, Sathish9098, adriro, auditor0517, bin2chen, btk, joestakey, juancito
103.4639 USDC - $103.46
returnedAmount
is wrong for delayed withdrawal in KangarooVault
and LiquidityPool
current.returnedAmount = availableFunds;
In KangarooVault.processWithdrawalQueue
, when funds are not enough to return, returnedAmount
is set to availableFunds
. But the queued withdrawal will be processed in the future again, so the update is not correct.
The following is the correct implementation.
current.returnedAmount += availableFunds;
returnedAmount
is wrong, but it is not used in the codebase, the impact is low.
depositFee
should be validated in LiquidityPool
There is no validation about depositFee
in LiquidityPool.setFees
. But if depositFee > 1e18, deposit
will revert and won't work.
uint256 fees = amount.mulWadDown(depositFee); uint256 amountForTokens = amount - fees;
LiquidityPool.processDeposits
reverts when count
= 0 and queuedDepositHead
= 0assert(queuedDepositHead + count - 1 < nextQueuedDepositId);
LiquidityPool.processDeposits
reverts when count
= 0 and queuedDepositHead
= 0 due to underflow, while it should work without reversion.
usedFunds
is not validated in LiquidityPool.hedgePositions
usedFunds += marginDelta;
In hedgePositions
, marginDelta
can be positive so usedFunds
can be increased. In the case, usedFunds
should be validated that usedFunds
<= totalFunds
.
VaultToken.setVault
can be front-runVaultToken.setVault
don't have any permission to set vault and only accepts the first call. So if admin don't set the vault right after it is constructed in the same block, an attacker can front-run and call this function to cause a DOS attack.
maxDepositAmount
is not used in KangarooVault
The storage variable maxDepositAmount
is only set and not used in KangarooVault
.
safeApprove
don't check allowance.SUSD.safeApprove(address(LIQUIDITY_POOL), maxCost); uint256 totalCost = EXCHANGE.closeTrade(tradeParams);
In KangarooVault._clearPendingOpenOrders
, it approves maxCost
, but only totalCost
is consumed in LIQUIDITY_POOL
.
totalCost
can be less than maxCost
. So if safeApprove
is from OpenZeppelin, _clearPendingOpenOrders
will work only once because OpenZeppelin
's safeApprove
checks allowance. But solmate's safeApprove
doesn't check allowance and there is no problem in _clearPendingOpenOrders
at the moment. If there is a plan to replace solmate's SafeTransferLib
by OpenZeppelin's, this problem should be considered.
#0 - JustDravee
2023-03-26T20:55:52Z
#1 - c4-judge
2023-05-03T03:36:37Z
JustDravee marked the issue as grade-b