Platform: Code4rena
Start Date: 21/08/2023
Pot Size: $125,000 USDC
Total HM: 26
Participants: 189
Period: 16 days
Judge: GalloDaSballo
Total Solo HM: 3
Id: 278
League: ETH
Rank: 49/189
Findings: 5
Award: $287.59
π Selected for report: 0
π Solo Findings: 0
π Selected for report: klau5
Also found by: 0x3b, 0xCiphky, 0xDING99YA, 0xWaitress, 0xbranded, 0xc0ffEE, 0xklh, 0xsurena, 0xvj, ABA, AkshaySrivastav, Anirruth, Aymen0909, Baki, Blockian, BugzyVonBuggernaut, DanielArmstrong, Evo, GangsOfBrahmin, HChang26, Inspex, Jiamin, Juntao, Kow, Krace, KrisApostolov, LFGSecurity, LokiThe5th, Mike_Bello90, Norah, Nyx, QiuhaoLi, RED-LOTUS-REACH, SBSecurity, Snow24, SpicyMeatball, T1MOH, Tendency, Toshii, Udsen, Yanchuan, __141345__, ak1, asui, auditsea, ayden, bart1e, bin2chen, blutorque, carrotsmuggler, chaduke, chainsnake, circlelooper, clash, codegpt, crunch, degensec, dirk_y, ge6a, gjaldon, grearlake, jasonxiale, juancito, ke1caM, kodyvim, kutugu, ladboy233, lanrebayode77, mahdikarimi, max10afternoon, mert_eren, nirlin, nobody2018, oakcobalt, parsely, peakbolt, pks_, pontifex, ravikiranweb3, rokinot, rvierdiiev, said, savi0ur, sces60107, sh1v, sl1, spidy730, tapir, tnquanghuy0512, ubermensch, visualbits, volodya, wintermute
0.0098 USDC - $0.01
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L201 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L359 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L774
When the admin of RdpxV2Core
calls RdpxV2Core.settle
. PerpetualAtlanticVault.settle
would be called to settle the put options. It would transfer rdpx from RdpxV2Core
to perpetualAtlanticVaultLP
and transfer weth from perpetualAtlanticVaultLP
to RdpxV2Core
. Afterwards, it invokes perpetualAtlanticVaultLP.subtractLoss
to record the decrease in weth. However, the strict equality check in perpetualAtlanticVaultLP.subtractLoss
makes itself vulnerable to potential DoS attack.
PerpetualAtlanticVault.settle
calculates the weth amount transferring from PerpetualAtlanticVaultLP
. And it calls PerpetualAtlanticVaultLP.subtractLoss
to record the loss.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L359
function settle( uint256[] memory optionIds ) external nonReentrant onlyRole(RDPXV2CORE_ROLE) returns (uint256 ethAmount, uint256 rdpxAmount) { β¦ IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).subtractLoss( ethAmount ); β¦ }
PerpetualAtlanticVaultLP.subtractLoss
has a strict equality check collateral.balanceOf(address(this)) == _totalCollateral - loss
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L201
function subtractLoss(uint256 loss) public onlyPerpVault { require( collateral.balanceOf(address(this)) == _totalCollateral - loss, "Not enough collateral was sent out" ); _totalCollateral -= loss; }
It seems to be a reasonable check. But the problems is that collateral.balanceOf(address(this))
can be attacked by directly sending 1 wei of weth to PerpetualAtlanticVaultLP
. _totalCollateral
wonβt increase when directly sending weth. The attacker can easily make the check always fail.
Manual Review
There two methods can mitigate the issues depending on what Dopex really wants to do:
collateral.balanceOf(address(this)) == _totalCollateral - loss
to collateral.balanceOf(address(this)) >= _totalCollateral - loss
collateral.balanceOf(address(this)) == _totalCollateral - loss
to _totalCollateral >= loss
DoS
#0 - c4-pre-sort
2023-09-09T09:54:55Z
bytes032 marked the issue as duplicate of #619
#1 - c4-pre-sort
2023-09-11T16:14:25Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T19:31:11Z
GalloDaSballo marked the issue as satisfactory
π Selected for report: said
Also found by: 0Kage, 0xCiphky, 0xkazim, 836541, AkshaySrivastav, Evo, HChang26, HHK, KrisApostolov, Neon2835, QiuhaoLi, Tendency, Toshii, bart1e, bin2chen, carrotsmuggler, chaduke, etherhood, gjaldon, glcanvas, josephdara, lanrebayode77, mahdikarimi, max10afternoon, nobody2018, peakbolt, qpzm, rvierdiiev, sces60107, tapir, ubermensch, volodya
17.313 USDC - $17.31
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L125 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L274 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L515 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L480
When calling PerpetualAtlanticVaultLP.deposit()
, it calls previewDeposit()
to calculate the minted shares to the receiver. However, it doesnβt call perpetualAtlanticVault.updateFunding()
first, leading to the unfair shares for the early depositors.
Users can call PerpetualAtlanticVaultLP.deposit()
to deposit collateral and mint LP tokens.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L125
function deposit( uint256 assets, address receiver ) public virtual returns (uint256 shares) { // Check for rounding error since we round down in previewDeposit. require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); perpetualAtlanticVault.updateFunding(); // Need to transfer before minting or ERC777s could reenter. collateral.transferFrom(msg.sender, address(this), assets); _mint(receiver, shares); _totalCollateral += assets; emit Deposit(msg.sender, receiver, assets, shares); }
It uses previewDeposit
to calculate the minted shares. And previewDeposit
calls convertToShares
to do the actual calculation.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L274
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L480
function convertToShares( uint256 assets ) public view returns (uint256 shares) { uint256 supply = totalSupply; uint256 rdpxPriceInAlphaToken = perpetualAtlanticVault.getUnderlyingPrice(); uint256 totalVaultCollateral = totalCollateral() + ((_rdpxCollateral * rdpxPriceInAlphaToken) / 1e8); return supply == 0 ? assets : assets.mulDivDown(supply, totalVaultCollateral); }
Afterwards, deposit()
calls perpetualAtlanticVault.updateFunding()
. It would transfer the funding from perpetualAtlanticVault
to PerpetualAtlanticVaultLP
. It increases totalVaultCollateral
.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L515
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L480
function updateFunding() public { updateFundingPaymentPointer(); β¦ collateralToken.safeTransfer( addresses.perpetualAtlanticVaultLP, (currentFundingRate * (block.timestamp - startTime)) / 1e18 ); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).addProceeds( (currentFundingRate * (block.timestamp - startTime)) / 1e18 ); β¦ } function updateFundingPaymentPointer() public { while (block.timestamp >= nextFundingPaymentTimestamp()) { if (lastUpdateTime < nextFundingPaymentTimestamp()) { β¦ collateralToken.safeTransfer( addresses.perpetualAtlanticVaultLP, (currentFundingRate * (nextFundingPaymentTimestamp() - startTime)) / 1e18 ); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP) .addProceeds( (currentFundingRate * (nextFundingPaymentTimestamp() - startTime)) / 1e18 ); β¦ } }
Suppose Alice and Bob both minted 100 shares by depositing 100 weth. And there are 100 weth funds remaining in perpetualAtlanticVault
. Those funds come from option premiums and RdpxV2Core funding. And Eve is going to call deposit().
assets.mulDivDown(supply, totalVaultCollateral) => 100*200 / 200 = 100
totalVaultCollateral
is now 400.The result is clearly unfair to early depositors. Later depositors should not earn the funds generated before their deposits.
Manual Review
PerpetualAtlanticVaultLP.deposit()
should call perpetualAtlanticVault.updateFunding()
before previewDeposit()
Timing
#0 - c4-pre-sort
2023-09-07T13:30:09Z
bytes032 marked the issue as duplicate of #1948
#1 - c4-pre-sort
2023-09-07T13:42:28Z
bytes032 marked the issue as duplicate of #867
#2 - c4-pre-sort
2023-09-11T09:05:18Z
bytes032 marked the issue as sufficient quality report
#3 - c4-judge
2023-10-20T19:23:51Z
GalloDaSballo marked the issue as satisfactory
π Selected for report: bin2chen
Also found by: 0Kage, 0xDING99YA, QiuhaoLi, Toshii, Yanchuan, carrotsmuggler, deadrxsezzz, ether_sky, flacko, gjaldon, kutugu, mert_eren, pep7siup, qpzm, said, sces60107, tapir, ubermensch
39.433 USDC - $39.43
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/reLP/ReLPContract.sol#L273
ReLPContract.reLP
calculates min amount of tokenA to be received before doing swap. However, the calculation is wrong.
ReLPContract.reLP
calculates min amount of tokenA to be received based on amountB
and tokenAInfo.tokenAPrice
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/reLP/ReLPContract.sol#L273
function reLP(uint256 _amount) external onlyRole(RDPXV2CORE_ROLE) { β¦ // calculate min amount of tokenA to be received mintokenAAmount = (((amountB / 2) * tokenAInfo.tokenAPrice) / 1e8) - (((amountB / 2) * tokenAInfo.tokenAPrice * slippageTolerance) / 1e16); uint256 tokenAAmountOut = IUniswapV2Router(addresses.ammRouter) .swapExactTokensForTokens( amountB / 2, mintokenAAmount, path, address(this), block.timestamp + 10 )[path.length - 1]; β¦
tokenAInfo.tokenAPrice
is the price of tokenA against tokenB. In other words, it indicates how many tokenB that one tokenA is equivalent to. It makes no sense to multiply the amount of tokenB with the price of tokenA against tokenB. It should multiply amountB with the price of tokenB against tokenA.
Manual Review
There are two ways to mitigate the issue.
(amountB / 2) / tokenAInfo.tokenAPrice
instead of (amountB / 2) * tokenAInfo.tokenAPrice)
Math
#0 - c4-pre-sort
2023-09-09T05:39:35Z
bytes032 marked the issue as duplicate of #1805
#1 - c4-pre-sort
2023-09-11T07:02:30Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-16T08:47:55Z
GalloDaSballo changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-10-20T09:23:37Z
GalloDaSballo marked the issue as satisfactory
π Selected for report: QiuhaoLi
Also found by: 0xDING99YA, 0xvj, SBSecurity, T1MOH, Toshii, Udsen, bart1e, bin2chen, carrotsmuggler, pep7siup, said, sces60107, wintermute
90.6302 USDC - $90.63
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L545 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1216 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1227
If minAmount
is zero in RdpxV2Core._curveSwap
. It would calculate the reasonable minOut
based on getDpxEthPrice
or getEthPrice
. However, it use the wrong price to calculate the minOut
if minAmount
is zero, RdpxV2Core._curveSwap
would calculate minOut
based on getDpxEthPrice
or getEthPrice
.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L545
function _curveSwap( uint256 _amount, bool _ethToDpxEth, bool validate, uint256 minAmount ) internal returns (uint256 amountOut) { β¦ // calculate minimum amount out uint256 minOut = _ethToDpxEth ? (((_amount * getDpxEthPrice()) / 1e8) - (((_amount * getDpxEthPrice()) * slippageTolerance) / 1e16)) : (((_amount * getEthPrice()) / 1e8) - (((_amount * getEthPrice()) * slippageTolerance) / 1e16)); // Swap the tokens amountOut = dpxEthCurvePool.exchange( _ethToDpxEth ? int128(int256(a)) : int128(int256(b)), _ethToDpxEth ? int128(int256(b)) : int128(int256(a)), _amount, minAmount > 0 ? minAmount : minOut ); }
_ethToDpxEth
should determine which price would be used. However, _curveSwap
implements it incorrectly. When _ethToDpxEth
is true, _amount
means the amount of eth. and minOut
should be the amount of DpxEth. The correct calculation should be:
(((_amount * (price of ETH against DpxEth)) / 1e8) - (((_amount *(price of ETH against DpxEth)) * slippageTolerance) / 1e16))
Therefore, getEthPrice()
should be used. But _curveSwap
uses getDpxEthPrice()
Manual Review
function _curveSwap( uint256 _amount, bool _ethToDpxEth, bool validate, uint256 minAmount ) internal returns (uint256 amountOut) { β¦ // calculate minimum amount out uint256 minOut = _ethToDpxEth - ? (((_amount * getDpxEthPrice()) / 1e8) - - (((_amount * getDpxEthPrice()) * slippageTolerance) / 1e16)) - : (((_amount * getEthPrice()) / 1e8) - - (((_amount * getEthPrice()) * slippageTolerance) / 1e16)); + ? (((_amount * getEthPrice()) / 1e8) - + (((_amount * getEthPrice()) * slippageTolerance) / 1e16)) + : (((_amount * getDpxEthPrice()) / 1e8) - + (((_amount * getDpxEthPrice()) * slippageTolerance) / 1e16)); // Swap the tokens amountOut = dpxEthCurvePool.exchange( _ethToDpxEth ? int128(int256(a)) : int128(int256(b)), _ethToDpxEth ? int128(int256(b)) : int128(int256(a)), _amount, minAmount > 0 ? minAmount : minOut ); }
Math
#0 - c4-pre-sort
2023-09-10T07:37:26Z
bytes032 marked the issue as duplicate of #2172
#1 - c4-pre-sort
2023-09-12T04:25:26Z
bytes032 marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-09-12T04:38:02Z
bytes032 marked the issue as duplicate of #970
#3 - c4-judge
2023-10-18T12:34:37Z
GalloDaSballo marked the issue as satisfactory
#4 - c4-judge
2023-10-21T07:53:20Z
GalloDaSballo changed the severity to 2 (Med Risk)
π Selected for report: juancito
Also found by: 0xDING99YA, 0xTiwa, 0xkazim, 0xnev, ABA, ArmedGoose, Aymen0909, Bauchibred, Evo, IceBear, KrisApostolov, MohammedRizwan, Nikki, QiuhaoLi, T1MOH, Toshii, WoolCentaur, Yanchuan, __141345__, asui, bart1e, carrotsmuggler, catellatech, chaduke, codegpt, deadrxsezzz, degensec, dethera, dirk_y, erebus, ether_sky, gjaldon, glcanvas, jasonxiale, josephdara, klau5, kodyvim, ladboy233, lsaudit, minhquanym, parsely, peakbolt, pep7siup, rvierdiiev, said, savi0ur, sces60107, tapir, ubermensch, volodya, zzebra83
140.2087 USDC - $140.21
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L421 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L337 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L303
After an epoch has ended the admin of RdpxV2Core
calls calculateFunding
to calculate the funding generated at the last epoch. Then, calls payFunding
to send weth from RdpxV2Core
to PerpetualAtlanticVault
. However, if purchase
and settle
happens before payFunding
. It could lead to wrong calculation of generated funding and possible revert.
payFunding
computes amount
of options using optionsPerStrike[strike] - fundingPaymentsAccountedForPerStrike[latestFundingPaymentPointer][strike]
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L421
function calculateFunding( uint256[] memory strikes ) external nonReentrant returns (uint256 fundingAmount) { β¦ uint256 amount = optionsPerStrike[strike] - fundingPaymentsAccountedForPerStrike[latestFundingPaymentPointer][ strike ]; uint256 timeToExpiry = nextFundingPaymentTimestamp() - (genesis + ((latestFundingPaymentPointer - 1) * fundingDuration)); uint256 premium = calculatePremium( strike, amount, timeToExpiry, getUnderlyingPrice() ); β¦ }
purchase
increases both optionsPerStrike[strike]
and fundingPaymentsAccountedForPerStrike[latestFundingPaymentPointer][strike]
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L303
function purchase( uint256 amount, address to ) external nonReentrant onlyRole(RDPXV2CORE_ROLE) returns (uint256 premium, uint256 tokenId) { β¦ totalActiveOptions += amount; fundingPaymentsAccountedFor[latestFundingPaymentPointer] += amount; optionsPerStrike[strike] += amount; // record the number of options funding has been accounted for the epoch and strike fundingPaymentsAccountedForPerStrike[latestFundingPaymentPointer][ strike ] += amount; emit Purchase(strike, amount, premium, to, msg.sender); }
And settle
reduces only optionsPerStrike[strike]
.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L337
function settle( uint256[] memory optionIds ) external nonReentrant onlyRole(RDPXV2CORE_ROLE) returns (uint256 ethAmount, uint256 rdpxAmount) { β¦ optionsPerStrike[strike] -= amount; totalActiveOptions -= amount; β¦ }
Suppose there are two options purchased at epoch 1. Those two options has the same strike price βAβ. And the total amount of options is 200:
// at epoch 1 optionsPerStrike[A] = 200 fundingPaymentsAccountedForPerStrike[latestFundingPaymentPointer][A] = 200
After an epoch has ended and latestFundingPaymentPointer
is updated.
// at epoch 2 optionsPerStrike[A] = 200 fundingPaymentsAccountedForPerStrike[latestFundingPaymentPointer][A] = 0
If nothing happens before calculateFunding
. The amount
used to calculate funding should be 200;
// calculateFunding uint256 amount = optionsPerStrike[strike] - fundingPaymentsAccountedForPerStrike[latestFundingPaymentPointer][strike] = 200 - 0 = 200
However, if there is a new option with 200 amount purchased at epoch 2 and settle at the same epoch. And these actions happen before calculateFunding
. The funding becomes 0.
// purchase a new option with 200 amount and strike price is still A. optionsPerStrike[A] = 200 + 200 = 400 fundingPaymentsAccountedForPerStrike[latestFundingPaymentPointer][A] = 0 + 200 = 200 // settle the new option optionsPerStrike[A] = 400 - 200 = 200 // calculateFunding uint256 amount = optionsPerStrike[strike] - fundingPaymentsAccountedForPerStrike[latestFundingPaymentPointer][strike] = 200 - 200 = 0
Moreover, if the amount of new option is 300. calculateFunding
would revert.
// purchase a new option with 300 amount and strike price is still A. optionsPerStrike[A] = 200 + 300 = 500 fundingPaymentsAccountedForPerStrike[latestFundingPaymentPointer][A] = 0 + 300 = 300 // settle the new option optionsPerStrike[A] = 500 - 300 = 200 // calculateFunding uint256 amount = optionsPerStrike[strike] - fundingPaymentsAccountedForPerStrike[latestFundingPaymentPointer][strike] = 200 - 300 // revert.
Manual Review
The best way to mitigate the issue is ensuring that calculateFunding
is alway the first action at the beginning of epoch. But Iβm not sure if it is easy to do without large modification of codes. Since it needs to record all the active strike prices.
Timing
#0 - c4-pre-sort
2023-09-10T12:52:29Z
bytes032 marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-09-15T08:35:53Z
bytes032 marked the issue as primary issue
#2 - psytama
2023-09-25T15:46:33Z
Duplicate of issue 1072.
#3 - c4-sponsor
2023-09-25T15:46:37Z
psytama (sponsor) acknowledged
#4 - c4-judge
2023-10-15T17:53:02Z
GalloDaSballo marked the issue as duplicate of #1798
#5 - c4-judge
2023-10-20T11:53:19Z
GalloDaSballo changed the severity to QA (Quality Assurance)
#6 - c4-judge
2023-10-20T18:16:19Z
GalloDaSballo marked the issue as grade-a