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: 65/189
Findings: 3
Award: $186.97
🌟 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#L200-L203 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L347-L351 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#L191-L194
The RdpxV2Core.settle
function is used to settle the options
. This function calls the perpetualAtlanticVault.settle
function where the actual option settling logic is executed. The perpetualAtlanticVault.settle
function calculates the amount of collateral token
to be transferred to the RdpxV2Core
contract from the perpetualAtlanticVaultLP
contract. Once the collateral token amount
has been transferred to the RdpxV2Core
contract the perpetualAtlanticVaultLP
contract _totalCollateral
state variable is updated by subtracting the transferred amount inside the perpetualAtlanticVaultLP.subtractLoss
function. It is shown below:
require( collateral.balanceOf(address(this)) == _totalCollateral - loss, "Not enough collateral was sent out" ); _totalCollateral -= loss;
As it is evident from the above code snippet there is a require
statement with a conditional check to verify collateral.balanceOf(address(this)) == _totalCollateral - loss
. The issue is if this condition fails then the entire RdpxV2Core.settle
transaction will revert. Hence the options will not be able to be settled.
Since the above condition (collateral.balanceOf(address(this)) == _totalCollateral - loss
) is checking exact equality it can be easily broken by front running the RdpxV2Core.settle
transaction and depositing collateral token amount
to the perpetualAtlanticVaultLP
contract externally. Hence this will increase the collateral.balanceOf(address(this)
amount more than the _totalCollateral
amount. Hence the collateral.balanceOf(address(this)) == _totalCollateral - loss
condition will be false. As a result the RdpxV2Core.settle
transaction will revert. Hence an attacker can front run every RdpxV2Core.settle
function by depositing small amount of collateral token to the perpetualAtlanticVaultLP
contract and keep on reverting the RdpxV2Core.settle
transaction thus permanently blocking the RdpxV2Core
contract to settle the options
.
Furthermore when the perpetualAtlanticVaultLP.addProceeds
function was called to update the _totalCollateral
variable for the deposited collateral tokens during option purchasing, it performed the following require
check.
require( collateral.balanceOf(address(this)) >= _totalCollateral + proceeds, "Not enough collateral token was sent" ); _totalCollateral += proceeds;
It checks the >=
condition rather than ==
condition. Hence it can be derived that the contract accepts collateral.balanceOf(address(this))
can be greater than the _totalCollateral
amount. But in the perpetualAtlanticVaultLP.subtractLoss
it checks ==
condition which is erroneous.
require( collateral.balanceOf(address(this)) == _totalCollateral - loss, "Not enough collateral was sent out" );
collateralToken.safeTransferFrom( addresses.perpetualAtlanticVaultLP, addresses.rdpxV2Core, ethAmount );
IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).subtractLoss( ethAmount );
require( collateral.balanceOf(address(this)) >= _totalCollateral + proceeds, "Not enough collateral token was sent" );
Manual Review and VSCode
Hence it is recommended to update the conditional logic inside the require
statement of the perpetualAtlanticVaultLP.subtractLoss
to check for the >=
condition rather than the ==
check. This will ensure an attacker can not break the RdpxV2Core.settle
transaction by depositing a small collateral token amount to the perpetualAtlanticVaultLP
contract externally.
The modified require
statement is given below:
require( collateral.balanceOf(address(this)) >= _totalCollateral - loss, "Not enough collateral was sent out" );
Other
#0 - c4-pre-sort
2023-09-09T09:53:59Z
bytes032 marked the issue as duplicate of #619
#1 - c4-pre-sort
2023-09-11T16:14:17Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-21T07:15:51Z
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/core/RdpxV2Core.sol#L1233-L1242 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L605 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L669 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L673-L674 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1160 https://github.com/dopex-io/rdpx-eth-oracle/blob/5762c2339b1c45b87ff4db172e43cef4a0ff603a/src/RdpxEthOracle.sol#L250
The RdpxV2Core.getRdpxPrice()
function is used to return the price of rDPX against ETH. The value returned by the RdpxV2Core.getRdpxPrice()
is of very high importance since it is used for calculations in critical functions such as RdpxV2Core._transfer()
, RdpxV2Core.calculateBondCost()
, and RdpxV2Core._calculateAmounts
. And those functions are used in main protocol use case implemented in the bonding functions such as RdpxV2Core.bondWithDelegate()
and RdpxV2Core.bond()
.
The issue here is that the RdpxV2Core
contract functions treat the value returned from the getRdpxPrice
function to be in 1e8 precision as mentioned in its Natspec comments. But the returned value is actually in 1e18 precision. The getRdpxPriceInEth
function is implemented in the RdpxEthOracle
contract and its function implementation upgrades the precision to 1e18 before value is returned as shown below:
price = consult(token0, 1e18);
Hence all the critical functions which use the return value of getRdpxPrice
use the wrong precision for their calculations thus breaking the accounting of the entire protocol.
Even though the RdpxEthOracle.getRdpxPriceInEth
is not in scope for this audit, I included this as a high severity finding here because the returned Rdpx price
is very important for the internal calculations within the critical functions of the RdpxV2Core
contract.
/** * @notice Returns the price of rDPX against ETH * @dev Price is in 1e8 Precision * @return rdpxPriceInEth rDPX price in ETH **/ function getRdpxPrice() public view returns (uint256) { return IRdpxEthOracle(pricingOracleAddresses.rdpxPriceOracle) .getRdpxPriceInEth(); }
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1233-L1242
uint256 rdpxRequiredInWeth = (_rdpxRequired * getRdpxPrice()) / 1e8;
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L605
uint256 rdpxAmountInWeth = (_rdpxAmount * getRdpxPrice()) / 1e8;
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L669
uint256 extraRdpxToWithdraw = (discountReceivedInWeth * 1e8) / getRdpxPrice();
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L673-L674
uint256 rdpxPrice = getRdpxPrice();
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1160
price = consult(token0, 1e18);
Manual Review and VSCode
Hence it is recommended to either change the RdpxEthOracle.getRdpxPriceInEth
function to return the value in 1e8 precision by modifying the relevant line of code as shown below:
price = consult(token0, 1e8);
Else it is recommended to change the RdpxV2Core
functions which uses the getRdpxPrice()
returned value, to work with 1e18 precision which is the correct current precision of the returned value.
Other
#0 - c4-pre-sort
2023-09-09T05:26:03Z
bytes032 marked the issue as duplicate of #549
#1 - c4-pre-sort
2023-09-12T05:20:11Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T18:27:59Z
GalloDaSballo marked the issue as satisfactory
#3 - c4-judge
2023-10-20T18:28:12Z
GalloDaSballo changed the severity to 2 (Med Risk)
#4 - 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/amo/UniV2LiquidityAmo.sol#L372-L374 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/amo/UniV2LiquidityAmo.sol#L381-L383 https://github.com/dopex-io/rdpx-eth-oracle/blob/5762c2339b1c45b87ff4db172e43cef4a0ff603a/src/RdpxEthOracle.sol#L217
The UniV2LiquidityAmo.getLpTokenBalanceInWeth
function is used to return the LP token balance of the contract in weth. It calls the getLpPrice()
function which is expected to return the LP price
in 1e8 precision.
getLpPrice()
function calls the RdpxEthOracle.getLpPriceInEth
function to get the LP price value
.
The issue here is that the RdpxEthOracle.getLpPriceInEth
returned value is in 1e18
precision as shown below (code snippet taken from the RdpxEthOracle.getLpPriceInEth
function).
return (lpPriceIn112x112 * 1e18) >> 112;
But the UniV2LiquidityAmo.getLpTokenBalanceInWeth
calculates the LP token balance considering the getLpPrice()
returned value is in 1e8 precision as shown below:
return (lpTokenBalance * getLpPrice()) / 1e8;
Hence the returned value of UniV2LiquidityAmo.getLpTokenBalanceInWeth
is erroneous and would provide the wrong value for anyone using the UniV2LiquidityAmo.getLpTokenBalanceInWeth
function to read the LP token balance of the UniV2LiquidityAmo
contract in weth.
function getLpTokenBalanceInWeth() external view returns (uint256) { return (lpTokenBalance * getLpPrice()) / 1e8; }
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/amo/UniV2LiquidityAmo.sol#L372-L374
function getLpPrice() public view returns (uint256) { return IRdpxEthOracle(addresses.rdpxOracle).getLpPriceInEth(); }
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/amo/UniV2LiquidityAmo.sol#L381-L383
return (lpPriceIn112x112 * 1e18) >> 112;
Manual Review and VSCode
Hence it is recommended to modify the UniV2LiquidityAmo.getLpTokenBalanceInWeth
function as shown below to account for 1e18
precision.
return (lpTokenBalance * getLpPrice()) / 1e18;
Decimal
#0 - c4-pre-sort
2023-09-13T06:05:04Z
bytes032 marked the issue as duplicate of #549
#1 - c4-pre-sort
2023-09-13T06:05:09Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T18:28:03Z
GalloDaSballo marked the issue as satisfactory
#3 - c4-judge
2023-10-20T18:28:21Z
GalloDaSballo changed the severity to 3 (High Risk)
🌟 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-L549
The RdpxV2Core._curveSwap
function is used to swap token A to token B based on the value of the _ethToDpxEth
Boolean variable. _ethToDpxEth
value denotes whether swap is from ETH to dpxETH or dpxETH to ETH.
Before the actual dpxEthCurvePool.exchange
happens the minOut
value is calculated as slippage protection as shown below:
uint256 minOut = _ethToDpxEth ? (((_amount * getDpxEthPrice()) / 1e8) - (((_amount * getDpxEthPrice()) * slippageTolerance) / 1e16)) : (((_amount * getEthPrice()) / 1e8) - (((_amount * getEthPrice()) * slippageTolerance) / 1e16));
If the _ethToDpxEth is true
which means the swap is from Eth to dpxEth, the _amount
is multiplied by value returned from getDpxEthPrice()
. Here the _amount
is in Eth
and getDpxEthPrice()
value is in Eth/DpxEth
unit with 1e8 precision. Hence it is clear this calculation is wrong. Because we want to multiply the _amount
(Eth value) by the getEthPrice()
value and not by the getDpxEthPrice()
value. Because getEthPrice()
returns the dpxEth/Eth
value in 1e8 precision which is the correct value to use to multiply the Eth _amount
to obtain the minOut value in dpxEth
units.
Similarly if the _ethToDpxEth is false
which means the swap is from dpxEth to Eth, the _amount
is multiplied by value returned from getEthPrice()
. Here the _amount
is in dpxEth
and getEthPrice()
value is in dpxEth/Eth
unit with 1e8 precision. Hence it is clear this calculation is wrong. Because we want to multiply the _amount
(dpxEth value) by the getDpxEthPrice()
value and not by the getEthPrice()
value. Because getDpxEthPrice()
returns the Eth/dpxEth
value in 1e8 precision which is the correct value to use to multiply the dpxEth _amount
to obtain the minOut value in Eth
units.
Above error in calculating the value of minOut
could prompt the dpxEthCurvePool.exchange
transaction to revert since we are providing the wrong slippage protection value to the actual swap happening in the exchange
transaction.
This will prompt the _curveSwap
transaction to behave in unexpected manner thus breaking the protocol unexpectedly.
uint256 minOut = _ethToDpxEth ? (((_amount * getDpxEthPrice()) / 1e8) - (((_amount * getDpxEthPrice()) * slippageTolerance) / 1e16)) : (((_amount * getEthPrice()) / 1e8) - (((_amount * getEthPrice()) * slippageTolerance) / 1e16));
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L545-L549
Manual Review and VSCode
Hence it is recommended to correct the minOut
value in the RdpxV2Core._curveSwap
function as shown below:
uint256 minOut = _ethToDpxEth ? (((_amount * getEthPrice()) / 1e8) - (((_amount * getEthPrice()) * slippageTolerance) / 1e16)) : (((_amount * getDpxEthPrice()) / 1e8) - (((_amount * getDpxEthPrice()) * slippageTolerance) / 1e16));
The above change will ensure the correct minOut
value is calculated based on the Boolean value of _ethToDpxEth
thus ensuring successful execution of the RdpxV2Core._curveSwap
transaction.
Other
#0 - c4-pre-sort
2023-09-10T07:19:16Z
bytes032 marked the issue as duplicate of #2172
#1 - c4-pre-sort
2023-09-12T04:25:10Z
bytes032 marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-09-12T04:38:06Z
bytes032 marked the issue as duplicate of #970
#3 - c4-judge
2023-10-18T12:34:12Z
GalloDaSballo marked the issue as satisfactory
#4 - c4-judge
2023-10-21T07:53:20Z
GalloDaSballo changed the severity to 2 (Med Risk)