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: 41/189
Findings: 6
Award: $482.63
🌟 Selected for report: 1
🚀 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/PerpetualAtlanticVault.sol#L359-L361 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L200-L203 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L772-L774
A strict validation in PerpetualAtlanticVaultLP.subtractLoss()
makes it possible for anyone to disable RdpxV2Core.settle()
just by sending 1 WETH to PerpetualAtlanticVaultLP
.
The check collateral.balanceOf(address(this)) == _totalCollateral - loss
found in subtractLoss()
below can be easily made to fail by anyone just by sending 1 unit of collateral to PerpetualAtlanticVaultLP
. In this case, the collateral is WETH.
function subtractLoss(uint256 loss) public onlyPerpVault { require( collateral.balanceOf(address(this)) == _totalCollateral - loss, "Not enough collateral was sent out" ); _totalCollateral -= loss; }
PerpetualAtlanticVaultLP
does not provide any functions to re-sync _totalCollateral
with the WETH balance of the contract so this leads to permanent disabling of RdpxV2Core.settle()
. Only RdpxV2Core.settle()
is affected because it is the only external function that calls VaultLP.subtractLoss()
internally.
The Dopex protocol relies on settle()
to increase the percentage of dpxETH's backing reserves in WETH and bring back the peg of dpxETH
when rDPX
price drops. This essentially disables one of the core functionalities of the Peg Stability Module and makes it difficult for the protocol to maintain dpxETH's peg. This causes damage to the protocol and the dpxETH stablecoin that is difficult to quantify.
Manual Review
Change subtractLoss()
's validation to be less strict like so:
function subtractLoss(uint256 loss) public onlyPerpVault { require( collateral.balanceOf(address(this)) >= _totalCollateral - loss, "Not enough collateral was sent out" ); _totalCollateral -= loss; }
DoS
#0 - c4-pre-sort
2023-09-09T05:48:12Z
bytes032 marked the issue as primary issue
#1 - c4-pre-sort
2023-09-11T16:12:57Z
bytes032 marked the issue as sufficient quality report
#2 - c4-sponsor
2023-09-25T13:41:01Z
psytama (sponsor) confirmed
#3 - psytama
2023-09-25T13:41:25Z
Change the require statement in the subtractLoss function.
#4 - c4-judge
2023-10-20T19:28:04Z
GalloDaSballo marked the issue as selected for report
#5 - c4-judge
2023-10-20T19:28:32Z
GalloDaSballo marked issue #2081 as primary and marked this issue as a duplicate of 2081
#6 - c4-judge
2023-10-20T19:35:04Z
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#L118-L135 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L190-L196 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L145-L175 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L262-L266 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L218-L229
Earlier depositors in PerpetualAtlanticVaultLP
can wait for PerpetualAtlanticVault
to add proceeds which the vault does every time it updates funding. Once proceeds are added, these earlier depositors can redeem their shares which are now worth more since addProceeds()
does not increase the number of shares and shares price is based on the total collateral / total supply of shares
.
This issue can be reproduced with the following test:
function testInitialDepositorsStealWeth() external { address alice = address(1); setApprovals(alice); mintWeth(10 ether, alice); deposit(10 ether, alice); // PerpVault adds proceeds to VaultLP via updateFunding() or updateFundingPaymentPointer() mintWeth(100 ether, address(vault)); vm.startPrank(address(vault)); weth.transfer(address(vaultLp), 100 ether); vaultLp.addProceeds(100 ether); vm.stopPrank(); assertEq(vaultLp.totalCollateral(), 110 ether); // Alice redeems shares as soon as TotalCollateral has increased uint256 aliceShares = vaultLp.balanceOf(alice); vm.startPrank(alice); vaultLp.redeem(aliceShares, alice, alice); vm.stopPrank(); assertEq(vaultLp.totalCollateral(), 0); assertEq(weth.balanceOf(alice), 110 ether); }
Copy the test above to tests/perp-vault/Integration.t.sol
and run it with :
$ forge test --mt testInitialDepositorsStealWeth`
The test shows Alice as a depositor who has deposited prior to the PerpVault
adding proceeds to the VaultLP
. This example shows Alice starting off with 10 ether and ending up with 110 ether, 100 ether of which are from the PerpVault
's proceeds.
Manual Review, Forge Test
So that earlier depositors can not immediately withdraw the WETH deposited by the PerpVault
, the PerpVault
can use deposit()
instead of doing a direct transfer of WETH and then call addProceeds()
. If the PerpVault
would like to distribute the WETH to depositors, they can instead burn()
their shares to decrease the total supply of shares and increase their price. This requires a burn()
function to be added to VaultLP
which is only accessible to the PerpVault
.
ERC4626
#0 - c4-pre-sort
2023-09-09T06:02:44Z
bytes032 marked the issue as primary issue
#1 - c4-pre-sort
2023-09-12T05:41:32Z
bytes032 marked the issue as sufficient quality report
#2 - bytes032
2023-09-12T05:42:38Z
I believe its related to #867, #412 and #409
#3 - c4-pre-sort
2023-09-14T07:17:11Z
bytes032 marked the issue as remove high or low quality report
#4 - c4-pre-sort
2023-09-14T07:17:21Z
bytes032 marked the issue as sufficient quality report
#5 - c4-sponsor
2023-09-25T13:32:08Z
psytama (sponsor) disputed
#6 - psytama
2023-09-25T13:32:19Z
This is intended behavior.
#7 - c4-judge
2023-10-20T08:07:53Z
GalloDaSballo changed the severity to 2 (Med Risk)
#8 - GalloDaSballo
2023-10-20T08:08:36Z
The finding demonstrates how yield can be stolen by laggard depositors, these depositors can contribute nothing to the yield and receive a share of it
#9 - c4-judge
2023-10-20T11:39:02Z
GalloDaSballo marked the issue as selected for report
#10 - c4-judge
2023-10-20T19:01:08Z
GalloDaSballo marked the issue as duplicate of #867
#11 - c4-judge
2023-10-20T19:56:35Z
GalloDaSballo changed the severity to 3 (High Risk)
#12 - c4-judge
2023-10-21T16:01:58Z
GalloDaSballo marked the issue as not selected for report
#13 - c4-judge
2023-10-30T20:01:06Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: LokiThe5th
Also found by: 0xPsuedoPandit, 0xTiwa, 0xnev, 0xvj, Evo, Jiamin, Juntao, QiuhaoLi, T1MOH, Udsen, circlelooper, crunch, eeshenggoh, gjaldon, hals, josephdara, kutugu, minhtrng, niki, umarkhatab_465
96.3292 USDC - $96.33
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L268-L284 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L118-L135 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L364-L366
Price of VaultLP
shares significantly decrease after RDPX collateral is added by PerpVault
due to incorrect decimals conversion. This impacts depositors who deposit after RDPX collateral is added to the VaultLP
(RDPX balance of VaultLP goes from 0 to non-zero). Post-RDPX collateral depositors will get minted significantly less shares and will lose a portion of the WETH they deposited to Pre-RDPX collateral depositors.
Add the below test to tests/perp-vault/Integration.t.sol
and run it with forge test --mt testPostRdpxCollateralIssue
:
function testPostRdpxCollateralIssue() external { // 1 rdpx = 0.2 WETH priceOracle.updateRdpxPrice(0.2 ether); address alice = address(1); setApprovals(alice); mintWeth(10 ether, alice); deposit(10 ether, alice); // PerpVault adds rdpx to VaultLP mintRdpx(500 ether, address(vault)); vm.startPrank(address(vault)); rdpx.transfer(address(vaultLp), 500 ether); vaultLp.addRdpx(500 ether); vm.stopPrank(); // Bob deposits after RDPX collateral was added to VaultLP address bob = address(2); setApprovals(bob); mintWeth(10 ether, bob); deposit(10 ether, bob); uint256 aliceShares = vaultLp.balanceOf(alice); uint256 bobShares = vaultLp.balanceOf(bob); assertGt(aliceShares, bobShares); vm.prank(alice); vaultLp.redeem(aliceShares, alice, alice); vm.prank(bob); vaultLp.redeem(bobShares, bob, bob); assertGt(weth.balanceOf(alice), weth.balanceOf(bob)); assertGt(weth.balanceOf(alice), 10 ether); assertLt(weth.balanceOf(bob), 10 ether); assertGt(rdpx.balanceOf(alice), rdpx.balanceOf(bob)); console.log("Shares alice and bob", aliceShares, bobShares); console.log("Weth balance of alice and bob", weth.balanceOf(alice), weth.balanceOf(bob)); console.log("Rdpx balance of alice and bob", rdpx.balanceOf(alice), rdpx.balanceOf(bob)); }
The above test shows that Alice, who has deposited prior to the RDPX collateral being added to the VaultLP
, ends up with significantly more shares than Bob, who has deposited after the RDPX collateral was added to the VaultLP
. The difference in shares is so significant that Alice ends up with almost 2x as much WETH as she deposited. Bob, on the other hand, ends up with less than 1% of his WETH deposit.
This issue is cause by the incorrect decimals conversion in convertToShares()
which is called when a user deposit()
s.
uint256 totalVaultCollateral = totalCollateral() + ((_rdpxCollateral * rdpxPriceInAlphaToken) / 1e8); return supply == 0 ? assets : assets.mulDivDown(supply, totalVaultCollateral);
rdpxPrinceInAlphaToken
is an 18 decimals value and so is _rdpxCollateral
since RDPX token has 18 decimals. Multiplying both values will lead to a 36 decimals value but the calculation only divides it by 1e8 instead of 1e18. This bloats totalVaultCollateral
by at least 8 decimals. And since shares is computed as (assets * supply) / totalVaultCollateral
, the bloated totalVaultCollateral
leads to significantly less shares being minted.
Manual Review, Forge Test
Change the convertShares()
code to:
uint256 totalVaultCollateral = totalCollateral() + ((_rdpxCollateral * rdpxPriceInAlphaToken) / 1e18);
This corrects the conversion and no longer bloats totalVaultCollateral
.
ERC4626
#0 - c4-pre-sort
2023-09-09T06:03:46Z
bytes032 marked the issue as primary issue
#1 - c4-pre-sort
2023-09-12T06:34:40Z
bytes032 marked the issue as sufficient quality report
#2 - bytes032
2023-09-12T06:34:48Z
Related to #549
#3 - c4-sponsor
2023-09-25T13:32:46Z
psytama (sponsor) disputed
#4 - psytama
2023-09-25T13:33:26Z
The price update in the contracts was meant to be in 1e8 precision. so the error is in the price oracle and not in the contracts using the oracle.
#5 - GalloDaSballo
2023-10-09T13:32:51Z
I believe that because of the coded POC, while the root cause is in the oracle, the finding is valid as it shows a valid impact
Will have to dig deeper
#6 - c4-judge
2023-10-15T18:40:00Z
GalloDaSballo marked the issue as duplicate of #549
#7 - GalloDaSballo
2023-10-15T18:41:54Z
After reading these reports, I have concluded that the root case is the decimals assumption on the getRdpxPriceInEth
function
Other wardens have sent the issue as a single finding that groups these underlying impacts
Due to this, while I have considered keeping the impacts separately
Since they are all caused by the decimals of getRdpxPriceInEth
and there's a finding that reports it as a single issue, I'm grouping them in that way
#8 - c4-judge
2023-10-20T18:27:47Z
GalloDaSballo marked the issue as satisfactory
#9 - c4-judge
2023-10-20T18:28:12Z
GalloDaSballo changed the severity to 2 (Med Risk)
#10 - c4-judge
2023-10-20T18:28:21Z
GalloDaSballo changed the severity to 3 (High Risk)
🌟 Selected for report: LokiThe5th
Also found by: 0xPsuedoPandit, 0xTiwa, 0xnev, 0xvj, Evo, Jiamin, Juntao, QiuhaoLi, T1MOH, Udsen, circlelooper, crunch, eeshenggoh, gjaldon, hals, josephdara, kutugu, minhtrng, niki, umarkhatab_465
96.3292 USDC - $96.33
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L335 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L296-L300 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L921
There is a decimal issue in settle()
that bloats the amount of WETH being withdrawn from VaultLP
. This over-withdrawal of WETH comes at the expense of liquidity providers in the VaultLP who staked their WETH to protect the floor price of dpxETH.
This leads to _activeCollateral
no longer being accurate and reflecting a much smaller amount than the total number of locked collateral in Put Options. There is no way to update _activeCollateral
directly to fix the issue. This leads to being unable to settle()
some of the remaining Options even if Admin sends back the over-withdrawn WETH back to the VaultLP. Also, sending back the over-withdrawn WETH without increasing the _activeCollateral
will mean depositors will be able to withdraw more collateral than they are supposed to.
Overall, this puts the protocol in a bad state and makes the Put Options feature unreliable.
The steps that lead to the issue are:
purchase()
calculations are correct and Put options are required, options are purchased and minted every time users bond()
. Purchasing options would lock some WETH collateral in the VaultLP
to ensure that Put Options can be exercised once they are In-The-Money.settle()
on these Options that are In-The-Money. As long as there are enough WETH in VaultLP, this leads to the over-withdrawal of WETH and the inaccurate accounting of _activeCollateral
.This is because settle()
makes an incorrect decimal conversion when calculating the WETH amount it is supposed to withdraw from the VaultLP
when exercising the Put Options:
ethAmount += (amount * strike) / 1e8;
amount
and strike
above are both fixed-point 18-decimal values but they are only being divided by 1e8 instead of 1e18. This leads to ethAmount
being bloated by 10 decimals. strike
is the RDPX price in WETH which is in 18 decimals as is stated in the comments of the RdpxEthOracle.getRdpxPriceInEth()
function that it relies on:
/// @notice Returns the price of rDPX in ETH /// @return price price of rDPX in ETH in 1e18 decimals function getRdpxPriceInEth() external view override returns (uint price) {
amount
is the amount of RDPX required for bonding which would also be 18 decimals since RDPX is an 18-decimal token.
The damage caused by this miscalculation bloating ethAmount
is in settle()
withdrawing and unlocking more WETH than it should:
// Transfer collateral token from perpetual vault to rdpx rdpxV2Core collateralToken.safeTransferFrom( addresses.perpetualAtlanticVaultLP, addresses.rdpxV2Core, ethAmount ); // Transfer rdpx from rdpx rdpxV2Core to perpetual vault IERC20WithBurn(addresses.rdpx).safeTransferFrom( addresses.rdpxV2Core, addresses.perpetualAtlanticVaultLP, rdpxAmount ); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).subtractLoss( ethAmount ); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP) .unlockLiquidity(ethAmount);
Manual Review
Change settle()
's computation of ethAmount
with the following code:
ethAmount += (amount * strike) / 1e8;
Decimal
#0 - c4-pre-sort
2023-09-09T17:10:01Z
bytes032 marked the issue as duplicate of #549
#1 - c4-pre-sort
2023-09-12T05:22:45Z
bytes032 marked the issue as not a duplicate
#2 - c4-pre-sort
2023-09-12T05:22:52Z
bytes032 marked the issue as sufficient quality report
#3 - bytes032
2023-09-12T05:22:56Z
Related to #549
#4 - c4-sponsor
2023-09-25T16:19:26Z
witherblock (sponsor) acknowledged
#5 - witherblock
2023-09-25T16:19:39Z
Duplicate of #549
#6 - c4-judge
2023-10-15T18:40:10Z
GalloDaSballo marked the issue as duplicate of #549
#7 - GalloDaSballo
2023-10-15T18:42:06Z
See #397
#8 - c4-judge
2023-10-20T18:27:51Z
GalloDaSballo marked the issue as satisfactory
#9 - c4-judge
2023-10-20T18:28:21Z
GalloDaSballo changed the severity to 3 (High Risk)
🌟 Selected for report: LokiThe5th
Also found by: 0xPsuedoPandit, 0xTiwa, 0xnev, 0xvj, Evo, Jiamin, Juntao, QiuhaoLi, T1MOH, Udsen, circlelooper, crunch, eeshenggoh, gjaldon, hals, josephdara, kutugu, minhtrng, niki, umarkhatab_465
96.3292 USDC - $96.33
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L539-L551 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L286 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L429-L434
When Core calls purchase()
, it is overpaying the premium for the Put Options by 1e10x. What it is paying is so bloated that the amount is even much larger than the value it is backing.
The below test can be added to tests/perp-vault/Unit.t.sol
to make the issue more obvious:
import { IOptionPricing } from "contracts/interfaces/IOptionPricing.sol"; import { OptionPricingSimple } from "contracts/libraries/OptionPricingSimple.sol"; import "forge-std/console.sol"; // import statements above are necessary for the test to run
function testCalculatePremiumIssue() external { IOptionPricing _optionPricing = new OptionPricingSimple(100, 1e9); vm.prank(address(vault)); weth.approve(address(vaultLp), 0); vault.setAddresses( address(_optionPricing), address(priceOracle), address(volOracle), address(1), address(rdpx), address(vaultLp), address(this) ); uint256 timeTillExpiry = 7 days; uint256 premium = vault.calculatePremium( 0.015 ether, // 1e18 because rdpxPrice is 18 decimals 500 ether, timeTillExpiry, 0.02 ether // 1e18 because rdpxPrice is 18 decimals ); console.log("Premium: ", premium); }
In the above test, we replace the MockOptionPricing
contract with the OptionPricingSimple
contract that comes with the repo. The test does not do any assertions since we are only interested to see the log output of calculatePremium()
which is:
Premium: 10000000000000000000000000000
That output is the premium that Core is paying for in WETH, which amounts to 1e28 WETH. That means the premium is 10M Ether, which is clearly overpriced. The issue lies in the decimal conversion in calculatePremium()
which uses 1e8 instead of 1e18:
function calculatePremium( uint256 _strike, uint256 _amount, uint256 timeToExpiry, uint256 _price ) public view returns (uint256 premium) { premium = ((IOptionPricing(addresses.optionPricing).getOptionPrice( _strike, _price > 0 ? _price : getUnderlyingPrice(), getVolatility(_strike), timeToExpiry ) * _amount) / 1e8); }
If 1e18 was used, we would get 1e18 (1 Ether) premium instead, which is a more realistic value for a 10 Ether Total Spot Price (500 ether * 0.02 ether) worth of Option.
Manual Review, Forge Test
Change the divisor in calculatePremium()
from 1e8 to 1e18 like so:
function calculatePremium( uint256 _strike, uint256 _amount, uint256 timeToExpiry, uint256 _price ) public view returns (uint256 premium) { premium = ((IOptionPricing(addresses.optionPricing).getOptionPrice( _strike, _price > 0 ? _price : getUnderlyingPrice(), getVolatility(_strike), timeToExpiry ) * _amount) / 1e18); }
Decimal
#0 - c4-pre-sort
2023-09-09T05:33:11Z
bytes032 marked the issue as duplicate of #549
#1 - c4-pre-sort
2023-09-12T05:20:51Z
bytes032 marked the issue as not a duplicate
#2 - c4-pre-sort
2023-09-12T05:20:56Z
bytes032 marked the issue as sufficient quality report
#3 - bytes032
2023-09-12T05:21:01Z
Related to #549
#4 - c4-sponsor
2023-09-25T14:07:34Z
psytama (sponsor) disputed
#5 - psytama
2023-09-25T14:07:53Z
The contract works as it is supposed to the error is in the pricing oracle.
#6 - c4-judge
2023-10-15T18:40:01Z
GalloDaSballo marked the issue as duplicate of #549
#7 - GalloDaSballo
2023-10-15T18:42:00Z
See #397
#8 - c4-judge
2023-10-20T18:27:53Z
GalloDaSballo marked the issue as satisfactory
#9 - c4-judge
2023-10-20T18:28:12Z
GalloDaSballo changed the severity to 2 (Med Risk)
#10 - c4-judge
2023-10-20T18:28:21Z
GalloDaSballo changed the severity to 3 (High Risk)
🌟 Selected for report: LokiThe5th
Also found by: 0xPsuedoPandit, 0xTiwa, 0xnev, 0xvj, Evo, Jiamin, Juntao, QiuhaoLi, T1MOH, Udsen, circlelooper, crunch, eeshenggoh, gjaldon, hals, josephdara, kutugu, minhtrng, niki, umarkhatab_465
96.3292 USDC - $96.33
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L699-L744 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L819-L885 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L598-L615
WETH delegators provide their WETH so that other users that only have RDPX can use the delegated WETH in the pool to bond. In exchange, WETH delegators charge a fee for the usage of their WETH. Due to issues with the calculation of the receipt tokens, the Delegatee (RDPX provider) would get 10-20x more receipt tokens than the Delegator. If the calculations were correct, the Delegator would get more receipt tokens because of the delegate fee.
_calculateAmounts()
is used in bondWithDelegate()
to calculate the amount of receipt tokens the Delegator (WETH provider) and the Delegatee (RDPX provider) are getting.
We copy the _calculateAmounts()
logic and put it in a contract that we put in Remix:
//SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.19; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import "hardhat/console.sol"; contract Dopex { function _calculateAmounts() external view returns (uint256 amount1, uint256 amount2) { uint256 _wethRequired = 1 ether; uint256 _rdpxRequired = 5 ether; uint256 _amount = 2000 ether; uint256 _delegateFee = 5e8; uint256 rdpxPrice = 0.2 ether; // Commented below for better clarity uint256 rdpxRequiredInWeth = (_rdpxRequired * rdpxPrice) / 1e8; // amount required for delegatee amount1 = ((rdpxRequiredInWeth * _amount) / (rdpxRequiredInWeth + _wethRequired)); // account for delegate fee amount1 = (amount1 * (100e8 - _delegateFee)) / 1e10; amount2 = _amount - amount1; console.log("Delegatee Amount: ", amount1); console.log("Delegator Amount: ", amount2); } }
After deploying the above contract in Remix and then running the _calculateAmounts()
function, we get the following logs:
Delegatee Amount: 1899999999810000000018 Delegator Amount: 100000000189999999982
As can be seen, we are getting a much larger amount of 1,899 for the Delegatee while the Delegator gets only 100. That means that the Delegator would lose out on ~90% of the receipt tokens they were supposed to get and will experience a loss on the WETH they delegated. On the other hand, the Delegate would almost 10x their RDPX value and get ~10x the receipt tokens they were supposed to get.
This issue is caused by using the wrong divisor of 1e8 in _calculateAmounts()
:
uint256 rdpxRequiredInWeth = (_rdpxRequired * getRdpxPrice()) / 1e8;
Since getRdpxPrice()
and _rdpxRequired
are 18 decimals, the divisor should be 1e18 instead of 1e8.
Manual Review, Remix
We can fix it by replacing the divisor of 1e8 with 1e18 in _calculateAmounts()
like so:
-uint256 rdpxRequiredInWeth = (_rdpxRequired * getRdpxPrice()) / 1e8; +uint256 rdpxRequiredInWeth = (_rdpxRequired * getRdpxPrice()) / 1e18;
Decimal
#0 - c4-pre-sort
2023-09-09T04:16:22Z
bytes032 marked the issue as primary issue
#1 - c4-pre-sort
2023-09-09T05:10:25Z
bytes032 marked the issue as duplicate of #549
#2 - c4-pre-sort
2023-09-12T05:18:01Z
bytes032 marked the issue as not a duplicate
#3 - bytes032
2023-09-12T05:18:21Z
Related to #549
#4 - c4-pre-sort
2023-09-12T05:18:32Z
bytes032 marked the issue as sufficient quality report
#5 - c4-sponsor
2023-09-25T14:12:56Z
psytama (sponsor) disputed
#6 - psytama
2023-09-25T14:13:30Z
This issue exists because of the error in the price oracle contract and not the main contract so the code works as it should.
#7 - GalloDaSballo
2023-10-12T07:40:00Z
Agree with the Sponsor on the root cause, however the finding has a coded POC so it has validity, most likely dup of the main one
#8 - c4-judge
2023-10-15T18:40:06Z
GalloDaSballo marked the issue as duplicate of #549
#9 - GalloDaSballo
2023-10-15T18:42:03Z
See #397
#10 - c4-judge
2023-10-20T18:27:55Z
GalloDaSballo marked the issue as satisfactory
#11 - c4-judge
2023-10-20T18:28:12Z
GalloDaSballo changed the severity to 2 (Med Risk)
#12 - c4-judge
2023-10-20T18:28:21Z
GalloDaSballo changed the severity to 3 (High Risk)
🌟 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#L202-L312 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L930 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L873
Incorrect calculation of minimum out of Token B in reLP()
and minimum out of Token A will always revert bond()
and bondWithDelegate()
when reLP is active. This means that the re-LP feature can not be used to burn RDPX and keep its price up.
We need to correct the testReLpContract()
test by setting RDPX price to a 1e18 value. The test was incorrectly set to use 1e8. We can add the following code to the start of testReLpContract()
test:
// RDPX Price must be a 1e18 value since RDPX has 18 decimals rdpxPriceOracle.updateRdpxPrice(1e18);
Running the test now will return an UniswapV2Router: INSUFFICIENT_B_AMOUNT
error because we are passing a bloated mintokenBAmount
to UniswapV2Router.removeLiquidity()
of 35823626367177226988400000000
:
uint256 mintokenBAmount = ((tokenAToRemove * tokenAInfo.tokenAPrice) / 1e8) - ((tokenAToRemove * tokenAInfo.tokenAPrice) * liquiditySlippageTolerance) / 1e16;
35823626367177226988400000000
is larger by 10 decimals and this would lead to Core.bond()
and Core.bondWithDelegate()
to always fail when isReLPActive
is true.
minTokenAAmount
also returns a value that is bloated by 10 decimals:
mintokenAAmount = (((amountB / 2) * tokenAInfo.tokenAPrice) / 1e8) - (((amountB / 2) * tokenAInfo.tokenAPrice * slippageTolerance) / 1e16);
Calculations for both minTokenBAmount
and minTokenAAmount
will need to be fixed for reLP()
to work as intended.
Manual Review, Forge Test
Computation for minimum out of Token B can be replaced with:
-uint256 mintokenBAmount = ((tokenAToRemove * tokenAInfo.tokenAPrice) / -1e8) - -((tokenAToRemove * tokenAInfo.tokenAPrice) * liquiditySlippageTolerance) / -1e16; +uint256 mintokenBAmount = ((lpToRemove * reserveB) / totalLpSupply) * (1e8 - liquiditySlippageTolerance) / 1e8;
Apart from correcting the decimals, we are also replacing the wrong usage of tokenAToRemove * tokenAPrice
since that gives us Token A amounts instead of Token B amounts.
Computation for minimum out of Token A can be replaced with:
-mintokenAAmount = -(((amountB / 2) * tokenAInfo.tokenAPrice) / 1e8) - -(((amountB / 2) * tokenAInfo.tokenAPrice * slippageTolerance) / 1e16); +mintokenAAmount = +(((amountB / 2) * tokenAInfo.tokenAPrice) / 1e18) - +(((amountB / 2) * tokenAInfo.tokenAPrice * slippageTolerance) / 1e26);
For Token A's calculation, we only need to fix the decimals.
Once the fixes are applied, running the test should now fail with incorrect assertions which shows reLP()
ran without reverting.
Uniswap
#0 - c4-pre-sort
2023-09-09T17:09:03Z
bytes032 marked the issue as duplicate of #1805
#1 - c4-pre-sort
2023-09-14T06:45:19Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T09:26:25Z
GalloDaSballo marked the issue as satisfactory
310.3768 USDC - $310.38
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L162 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L262-L266 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L218-L229 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L346-L357
VaultLP
does not allow redemption of shares when it leads to 0 WETH being withdrawn. VaultLP
can end up in a state where it has a small amount of WETH and most of its reserves is in RDPX instead. This can happen when most of its WETH is locked as collateral for Put Options and all these Put Options were exercised/settled due to RDPX price dropping.
In this state, many depositors in the LP who have lost their WETH will not even be able to redeem their shares for the RDPX compensation.
The following steps will reproduce the issue:
deposit()
100_000e18
WETH to VaultLP
RdpxV2Core
purchased so many options (since so many users were bonding) that it locked up all 100_000e18
WETH collateral as backing for all the options.settled()
to bring dpxETH peg back up. This replaces all the 100_000e18
WETH with RDPX.VaultLP
is now 0, none of the depositors can redeem()
their shares due to this check:require(assets != 0, "ZERO_ASSETS");
The above check will always fail since the computation for assets
looks like:
shares.mulDivDown(totalCollateral(), supply)
totalCollateral()
will return 0 because there is no WETH collateral left in the VaultLP
. So the above computation will be shares * 0 / supply
which will always equal 0. So even if the depositor has 1_000e18
shares and a lot of RDPX they are supposed to be able to withdraw, they will not be able to. In effect, they would lose 100% of their WETH deposited into VaultLP
and get zero RDXP in compensation.
Manual Review
The validation in redeem()
can be replaced with the following:
require((assets + rdpxAmount) != 0, "ZERO_ASSETS");
That way, as long as there is any amount of RDPX or WETH they can withdraw, they will be able to do so.
ERC4626
#0 - c4-pre-sort
2023-09-15T05:47:19Z
bytes032 marked the issue as primary issue
#1 - c4-pre-sort
2023-09-15T05:47:25Z
bytes032 marked the issue as sufficient quality report
#2 - c4-sponsor
2023-09-25T13:43:07Z
psytama (sponsor) confirmed
#3 - psytama
2023-09-25T13:43:20Z
modify require in the redeem function.
#4 - GalloDaSballo
2023-10-09T14:43:29Z
The finding is valid in that 0 weth will revert
But the scenario seems to be extreme, thinking Med due to reliance on external condition
#5 - c4-judge
2023-10-12T11:29:24Z
GalloDaSballo changed the severity to 2 (Med Risk)
#6 - c4-judge
2023-10-15T18:02:26Z
GalloDaSballo marked the issue as selected for report
🌟 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
19.1724 USDC - $19.17
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1169-L1173 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L705-L708 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L904-L907
When bond discount is higher than 51e8 due to a large bond discount factor, or large rDPX reserves in the treasury, or both, then bond()
and bondWithDelegate()
will be disabled when called with no rDPX decaying bond.
calculateBondCost()
is a public function that's called by both bond()
and bondWithDelegate()
. The issue happens because of the following code in calculateBondCost()
:
rdpxRequired = ((RDPX_RATIO_PERCENTAGE - (bondDiscount / 2)) * _amount * DEFAULT_PRECISION) / (DEFAULT_PRECISION * rdpxPrice * 1e2);
RDPX_RATIO_PERCENTAGE
is a constant with a value of 25e8. Since we subtract bondDiscount / 2
from RDPX_RATIO_PERCENTAGE
, then bondDiscount / 2
must be a value less than 25e8 or else the function will revert with an underflow error. However, nothing in calculateBondCost()
ensures that bondDiscount
is never more than 50e8.
The bond discount in calculateBondCost()
is computed as:
uint256 bondDiscount = (bondDiscountFactor * Math.sqrt(IRdpxReserve(addresses.rdpxReserve).rdpxReserve()) * 1e2) / (Math.sqrt(1e18)); // 1e8 precision _validate(bondDiscount < 100e8, 14);
The above calculations show that bond discount increases as bondDiscountFactor
and rdpxReserve
increase. It also validates that bondDiscount
should never be higher than 100e8. So a value of 51e8 to 99e8 should be valid values for bondDiscount
and yet they cause the computation for rdpxRequired
to revert with an underflow error.
Manual Review
If bondDiscount
is meant to be a percentage, then it should be multiplied by the total rdpx amount and subtracted from the total rdpx amount rather than subtracted directly from RDPX_RATIO_PERCENTAGE
. If it's not a percentage value, then it can be subtracted from the total rdpx amount.
The same fix can be applied to the wethRequired
calculation.
Under/Overflow
#0 - c4-pre-sort
2023-09-07T10:59:42Z
bytes032 marked the issue as duplicate of #245
#1 - c4-pre-sort
2023-09-11T12:02:28Z
bytes032 marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-09-15T06:57:51Z
bytes032 marked the issue as duplicate of #2084
#3 - c4-judge
2023-10-12T09:27:13Z
GalloDaSballo changed the severity to QA (Quality Assurance)