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: 44/189
Findings: 5
Award: $393.17
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Toshii
Also found by: 0x3b, 0xDING99YA, 0xmystery, Cosine, Jiamin, Juntao, Matin, Qeew, Topmark, catwhiskeys, circlelooper, crunch, deadrxsezzz, eeshenggoh, lsaudit, peakbolt, pep7siup, piyushshukla, qpzm, visualbits
96.3292 USDC - $96.33
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1189-L1190
roundUp rounds the price up with precision of 1e6, unfortunately the RDPX price also has small decimal number and the rounding actually inflates the price.
The oracle returns in with 8 decimals
roundUp rounds up with 6 decimals of precision
This all combined lead to an issue, because the current price of RDPX is 13.5 USD and the price of WETH is 1677 USD. This means that the oracle will return 0.008e8 as a price of RDPX represented in WETH. Now when we input this price into the strike formula this happens:
uint256 strike = IPerpetualAtlanticVault(addresses.perpetualAtlanticVault) .roundUp(rdpxPrice - (rdpxPrice / 4)); // 25% below the current price
(0.008e8 - (0.008e8 / 4)) => 0.006e8
And when we call roundUp on this value, It will try and perform this equation to get the remainder:
0.006e8 % 1e6 => 0.6e6 % 1e6 => 0.6e6
And afterwards it will turn to the else where it will return _strike - remainder + roundingPrecision
0.6e6 - 0.6e6 + 1e6 => 1e6
function roundUp(uint256 _strike) public view returns (uint256 strike) { uint256 remainder = _strike % roundingPrecision; if (remainder == 0) { return _strike; } else { return _strike - remainder + roundingPrecision; }
This actually makes the bond from put to call, as it increases the strike price above our current one.
Also found in purchase.
Place it in Unit.t.sol
function test_calculate_cost() public { address user1 = address(111); rdpxPriceOracle.updateRdpxPrice(8e5); // 0.008e8 vm.prank(user1); rdpxV2Core.calculateBondCost(1e18, 0); }
When run with -vvvv we see that we input 6e5 (0.008e8 - 25%) into roundUp and get 1e6.
│ ├─ [2876] PerpetualAtlanticVault::roundUp(600000 [6e5]) [staticcall] │ │ └─ ← 1000000 [1e6]
Manual review
Use lower precision for the rounding -- 1e4 as an example.
Math
#0 - c4-pre-sort
2023-09-09T06:26:25Z
bytes032 marked the issue as primary issue
#1 - c4-pre-sort
2023-09-09T10:14:38Z
bytes032 marked the issue as duplicate of #2083
#2 - c4-pre-sort
2023-09-12T04:43:52Z
bytes032 marked the issue as sufficient quality report
#3 - c4-judge
2023-10-20T14:16:10Z
GalloDaSballo marked the issue as satisfactory
🌟 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
subtractLoss require
statement makes it possible to DoS this function and makes PerpetualAtlanticVault settle unusable.
The require
statement in subtractLoss allows an attacker to exploit it by donating a small amount the collateral token, causing the require
condition to fail consistently.
require( collateral.balanceOf(address(this)) == _totalCollateral - loss, "Not enough collateral was sent out" );
The issue arises because this statement strictly checks if the collateral held by the contract ( as balance ) matches the storage variable _totalCollateral. By donating any amount of the collateral token to the contract, the value of _totalCollateral
remains unchanged, while collateral.balanceOf(address(this))
changes.
As this function is used in settle process, it can cause repeated reverting, leading to a DoS scenario and preventing the admin from settling any bond.
Manual review
You can modify the require statement from ==
to >=
, or use an another check method.
require( - collateral.balanceOf(address(this)) == _totalCollateral - loss, + collateral.balanceOf(address(this)) >= _totalCollateral - loss, "Not enough collateral was sent out" );
DoS
#0 - c4-pre-sort
2023-09-09T06:10:51Z
bytes032 marked the issue as duplicate of #619
#1 - c4-pre-sort
2023-09-11T16:14:02Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T19:36:29Z
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 recoverERC721 function is utilized to recover an NFT and transfer it to the V2Core. V2Core lacks the capability to return the NFT, as well as the functionality to utilize it for collecting LP rewards or removing/adding liquidity.
Although the likelihood to be called regularly is low, the existence of the recoverERC721 suggests that it is expected to be called upon when necessary.
The recoverERC721 within UniV3LiquidityAmo is designed for NFT recovery. However, in practice, it does the opposite. When invoked by an admin, the NFT is transferred to V2Core. While this might appear beneficial in theory, it's worth noting that V2Core is equipped with a function for receiving NFTs, but not for sending them. The only available withdrawal function is for ERC20 tokens (emergencyWithdraw), which won't work as it withdraws the balance of address(this)
rather than the NFT ID.
When the NFT is transferred, it becomes perpetually stuck. Furthermore, since this contract fails to implement the UNIv3 AMO, it remains unable to use the NFT to collectFees, addLiquidity or removeLiquidity.
Manual review
Add a function to also handle NFT withdraws to different addresses.
function emergencyWithdraw(address nft,address to, uint ID,) external onlyRole(DEFAULT_ADMIN_ROLE) { _whenPaused(); ERC721(nft).safeTransfer(to, ID); emit LogEmergencyNftWithdraw(msg.sender, nft,to,ID); }
Or make recoverERC721 to send the NFT to any address you need.
- function recoverERC721(address tokenAddress,uint256 token_id) external onlyRole(DEFAULT_ADMIN_ROLE) { + function recoverERC721(address tokenAddress,address to, uint256 token_id) external onlyRole(DEFAULT_ADMIN_ROLE) { INonfungiblePositionManager(tokenAddress).safeTransferFrom( address(this), - rdpxV2Core, + to, token_id ); emit RecoveredERC721(tokenAddress, token_id); }
ERC721
#0 - c4-pre-sort
2023-09-09T12:10:44Z
bytes032 marked the issue as duplicate of #106
#1 - c4-pre-sort
2023-09-12T06:10:15Z
bytes032 marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-09-12T06:11:17Z
bytes032 marked the issue as duplicate of #935
#3 - c4-judge
2023-10-20T18:05:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#4 - c4-judge
2023-10-20T18:05:58Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: juancito
Also found by: 0x3b, 0xmuxyz, 0xnev, ArmedGoose, Bauchibred, KrisApostolov, RED-LOTUS-REACH, Viktor_Cortess, ciphermarco, ginlee, ladboy233, mitko1111, nemveer
90.6302 USDC - $90.63
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/reLP/ReLPContract.sol#L291-L294
addLiquidity has a slippage input that in neglected in the current code, which combined with the non-existent deadline leads to losses and unpredictable trades.
addLiquidity has 2 parameters (amountAMin
and amountBMin
), that account for slippage losses, if the trade executes at a bad time, or protect against MEV. However in the current implementation, the slippage check in not accounted for and in it's place 0 is used, meaning any change in price will satisfy the call. This combined with no deadline (as block.timestamp
means the TX will pass anytime, 1h, 1 day or even 1 week after is sent) means that the trade can execute at unprecedented time, and unknown prices.
A likely scenario would be for the call to remain stuck in the mem-pool for some days, executing at bad prices - Protocol receives less LP
(, , uint256 lp) = IUniswapV2Router(addresses.ammRouter).addLiquidity( addresses.tokenA, addresses.tokenB, tokenAAmountOut, amountB / 2, 0, 0,//@audit should should be `amountBMin` address(this), block.timestamp + 10//@audit why? )
Manual review
You can either add minA
and minB
to the function parameters, or like in other contract implement slippage tolerance calculation. Here is my pseudo-code that you can add here, before the addLiquidity call:
uint minA = tokenAAmountOut - (tokenAAmountOut * slippageTolerance); uint minB = amountB / 2 - ((amountB / 2) * slippageTolerance);
Timing
#0 - c4-pre-sort
2023-09-07T12:50:18Z
bytes032 marked the issue as duplicate of #1259
#1 - c4-pre-sort
2023-09-11T07:51:14Z
bytes032 marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-09-11T07:52:47Z
bytes032 marked the issue as duplicate of #1032
#3 - c4-judge
2023-10-15T19:21:16Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: degensec
Also found by: 0x3b, 0xnev, HChang26, KmanOfficial, QiuhaoLi, T1MOH, WoolCentaur, Yanchuan, ayden, bart1e, jasonxiale, kutugu, mert_eren, nirlin, peakbolt, peanuts, pep7siup, qpzm, tapir, ubermensch, wintermute
24.8267 USDC - $24.83
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/reLP/ReLPContract.sol#L202
The reLP
function within the ReLPContract does not return excess amounts, potentially resulting in small amounts of WETH being left trapped within the contract.
The reLP
function includes an addliquidity
call to UniSwap v2. Subsequently, the function returns the remaining tokenA (RDPX) to the RdpxV2Core. However, it neglects to return the remaining tokenB (WETH) held within the contract. Over time, although the un-returned WETH accumulates, but remains permanently trapped within the contract.
Manual review
A simple solution would be to modify the code to also return the unused WETH by adding the following changes:
IERC20WithBurn(addresses.tokenA).safeTransfer( addresses.rdpxV2Core, IERC20WithBurn(addresses.tokenA).balanceOf(address(this)) ); + IERC20WithBurn(addresses.tokenB).safeTransfer( + addresses.rdpxV2Core, + IERC20WithBurn(addresses.tokenB).balanceOf(address(this)) + );
ETH-Transfer
#0 - c4-pre-sort
2023-09-07T12:49:55Z
bytes032 marked the issue as duplicate of #1286
#1 - c4-pre-sort
2023-09-11T15:37:54Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-18T12:14:42Z
GalloDaSballo marked the issue as satisfactory