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: 66/189
Findings: 3
Award: $181.45
🌟 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
Admin won't be able to settle the options.
The PerpetualAtlanticVaultLP.subtractLoss()
function is designed to handle loss deductions from the _totalCollateral
balance, and it ensures that the contract's collateral balance matches the expected balance after the loss deduction at line 201.
PerpetualAtlanticVaultLP.sol
function subtractLoss(uint256 loss) public onlyPerpVault { require( collateral.balanceOf(address(this)) == _totalCollateral - loss, "Not enough collateral was sent out" ); _totalCollateral -= loss; }
However, the attacker can directly transfer the collateral token to the PerpetualAtlanticVaultLP
contract which will not increase the _totalCollateral
balance. As a result, the contract's collateral balance won't match the expected balance after a loss deduction. This causes the subtractLoss()
function always to revert when called.
Since the RdpxV2Core.settle()
function includes a call to the PerpetualAtlanticVaultLP.subtractLoss()
function when the subtractLoss()
function fails, it will cause the entire transaction to revert. As a result, the admin won't be able to settle the options.
RdpxV2Core.sol
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L764-L783
function settle( uint256[] memory optionIds ) external onlyRole(DEFAULT_ADMIN_ROLE) returns (uint256 amountOfWeth, uint256 rdpxAmount) { _whenNotPaused(); (amountOfWeth, rdpxAmount) = IPerpetualAtlanticVault( addresses.perpetualAtlanticVault ).settle(optionIds); for (uint256 i = 0; i < optionIds.length; i++) { optionsOwned[optionIds[i]] = false; } reserveAsset[reservesIndex["WETH"]].tokenBalance += amountOfWeth; reserveAsset[reservesIndex["RDPX"]].tokenBalance -= rdpxAmount; emit LogSettle(optionIds); }
PerpetualAtlanticVaultLP.sol
function settle( uint256[] memory optionIds ) external nonReentrant onlyRole(RDPXV2CORE_ROLE) returns (uint256 ethAmount, uint256 rdpxAmount) { _whenNotPaused(); _isEligibleSender(); updateFunding(); for (uint256 i = 0; i < optionIds.length; i++) { uint256 strike = optionPositions[optionIds[i]].strike; uint256 amount = optionPositions[optionIds[i]].amount; // check if strike is ITM _validate(strike >= getUnderlyingPrice(), 7); ethAmount += (amount * strike) / 1e8; rdpxAmount += amount; optionsPerStrike[strike] -= amount; totalActiveOptions -= amount; // Burn option tokens from user _burn(optionIds[i]); optionPositions[optionIds[i]].strike = 0; } // 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); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).addRdpx( rdpxAmount ); emit Settle(ethAmount, rdpxAmount, optionIds); }
Manual Review
We suggest changing the validation of the collateral sent out tracking in subtractLoss()
function from ==
into >=
at line 201. For example:
PerpetualAtlanticVaultLP.sol
function subtractLoss(uint256 loss) public onlyPerpVault { require( - collateral.balanceOf(address(this)) == _totalCollateral - loss, + collateral.balanceOf(address(this)) >= _totalCollateral - loss, "Not enough collateral was sent out" ); _totalCollateral -= loss; }
DoS
#0 - c4-pre-sort
2023-09-09T10:00:50Z
bytes032 marked the issue as duplicate of #619
#1 - c4-pre-sort
2023-09-11T16:15:12Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T19:34:58Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: bart1e
Also found by: 0x3b, 0xCiphky, Aymen0909, HHK, Inspex, bin2chen, chaduke, gkrastenov, jasonxiale, josephdara, kodyvim, peakbolt, pep7siup, rokinot, rvierdiiev, tapir
181.367 USDC - $181.37
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/amo/UniV3LiquidityAmo.sol#L324-L336
The UniswapV3 NFT will stuck in the RdpxV2Core
contract forever, resulting in the admin not being able to close the position and retrive the liqudity back.
The UniV3LiquidityAmo.recoverERC721()
function is used to transfered the UniswapV3 NFT to the RdpxV2Core
contract.
UniV3LiquidityAmo.sol
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/amo/UniV3LiquidityAmo.sol#L324-L336
function recoverERC721( address tokenAddress, uint256 token_id ) external onlyRole(DEFAULT_ADMIN_ROLE) { // Only the owner address can ever receive the recovery withdrawal // INonfungiblePositionManager inherits IERC721 so the latter does not need to be imported INonfungiblePositionManager(tokenAddress).safeTransferFrom( address(this), rdpxV2Core, token_id ); emit RecoveredERC721(tokenAddress, token_id); }
However, there is no approval or transfer UniswapV3 NFT methods in the RdpxV2Core
contract. As a result, the NFT will be stuck in the RdpxV2Core
contract forever. Thus, the owner not being able to close the position and retrive the liqudity back since the owner of the NFT is RdpxV2Core
contract.
Manual Review
We suggest implementing the recovery UniswapV3 NFT method in the RdpxV2Core
contract.
ERC721
#0 - c4-pre-sort
2023-09-12T06:12:02Z
bytes032 marked the issue as duplicate of #935
#1 - c4-judge
2023-10-20T18:05:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#2 - c4-judge
2023-10-20T18:05:49Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: 0xrafaelnicolau
Also found by: 0x111, 0xCiphky, 0xMosh, 0xWaitress, 0xc0ffEE, 0xkazim, 0xnev, 0xvj, ABAIKUNANBAEV, Aymen0909, Baki, ElCid, HChang26, HHK, Inspex, Jorgect, Kow, Krace, KrisApostolov, LFGSecurity, MiniGlome, Nyx, QiuhaoLi, RED-LOTUS-REACH, Talfao, Toshii, Vagner, Viktor_Cortess, Yanchuan, _eperezok, asui, atrixs6, bart1e, bin2chen, carrotsmuggler, chaduke, chainsnake, deadrxsezzz, degensec, dethera, dimulski, dirk_y, ether_sky, gizzy, glcanvas, grearlake, gumgumzum, halden, hals, kodyvim, koo, ladboy233, lanrebayode77, max10afternoon, minhtrng, mussucal, nobody2018, peakbolt, pontifex, qbs, ravikiranweb3, rvierdiiev, said, tapir, ubermensch, volodya, wintermute, yashar, zaevlad, zzebra83
0.0734 USDC - $0.07
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L975-L990
The functions that using sync() in RdpxV2Core will always revert.
The withdraw()
function use for withdraw unused WETH of delegate.
RdpxV2Core.sol https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L975-L990
function withdraw( uint256 delegateId ) external returns (uint256 amountWithdrawn) { _whenNotPaused(); _validate(delegateId < delegates.length, 14); Delegate storage delegate = delegates[delegateId]; _validate(delegate.owner == msg.sender, 9); amountWithdrawn = delegate.amount - delegate.activeCollateral; _validate(amountWithdrawn > 0, 15); delegate.amount = delegate.activeCollateral; IERC20WithBurn(weth).safeTransfer(msg.sender, amountWithdrawn); emit LogDelegateWithdraw(delegateId, amountWithdrawn); }
However, the function does not decrease the totalWethDelegated state, this state was increased in the addToDelegate() function, Resulting in the sync() function will revert due to an arithmetic underflow when it calculate balance with totalWethDelegated because the balance of weth in contract is less than totalWethDelegated.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L941-L968
function addToDelegate( uint256 _amount, uint256 _fee ) external returns (uint256) { _whenNotPaused(); // fee less than 100% _validate(_fee < 100e8, 8); // amount greater than 0.01 WETH _validate(_amount > 1e16, 4); // fee greater than 1% _validate(_fee >= 1e8, 8); IERC20WithBurn(weth).safeTransferFrom(msg.sender, address(this), _amount); Delegate memory delegatePosition = Delegate({ amount: _amount, fee: _fee, owner: msg.sender, activeCollateral: 0 }); delegates.push(delegatePosition); // add amount to total weth delegated totalWethDelegated += _amount; emit LogAddToDelegate(_amount, _fee, delegates.length - 1); return (delegates.length - 1); }
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L995-L1008
function sync() external { for (uint256 i = 1; i < reserveAsset.length; i++) { uint256 balance = IERC20WithBurn(reserveAsset[i].tokenAddress).balanceOf( address(this) ); if (weth == reserveAsset[i].tokenAddress) { balance = balance - totalWethDelegated; } reserveAsset[i].tokenBalance = balance; } emit LogSync(); }
The sync() function was used in both the reLP() and _sendTokensToRdpxV2Core() functions, as a result, these functions will become unusable.
Manual Review
Adding the deduction of the totalWethDelegated state in the withdraw() function.
RdpxV2Core.sol
function withdraw( uint256 delegateId ) external returns (uint256 amountWithdrawn) { _whenNotPaused(); _validate(delegateId < delegates.length, 14); Delegate storage delegate = delegates[delegateId]; _validate(delegate.owner == msg.sender, 9); amountWithdrawn = delegate.amount - delegate.activeCollateral; _validate(amountWithdrawn > 0, 15); delegate.amount = delegate.activeCollateral; + totalWethDelegated -= amountWithdrawn; IERC20WithBurn(weth).safeTransfer(msg.sender, amountWithdrawn); emit LogDelegateWithdraw(delegateId, amountWithdrawn); }
DoS
#0 - c4-pre-sort
2023-09-08T13:27:29Z
bytes032 marked the issue as duplicate of #2186
#1 - c4-judge
2023-10-20T17:55:32Z
GalloDaSballo changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-10-20T17:55:41Z
GalloDaSballo marked the issue as satisfactory
#3 - c4-judge
2023-10-21T07:38:54Z
GalloDaSballo changed the severity to 3 (High Risk)
#4 - c4-judge
2023-10-21T07:45:04Z
GalloDaSballo marked the issue as partial-50