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: 80/189
Findings: 4
Award: $101.13
🌟 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/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L201 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVault.sol#L359 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/core/RdpxV2Core.sol#L772-L774
The RpdxV2Core.settle
function can be blocked due to the PerpetualAtlanticVaultLP
contract balance of collateral
being higher than expected.
The RpdxV2Core.settle
function calls the PerpetualAtlanticVault.settle
function which calls the PerpetualAtlanticVaultLP.subtractLoss
with a strict requirement:
200 require( 201 collateral.balanceOf(address(this)) == _totalCollateral - loss, 202 "Not enough collateral was sent out" 203 );
The PerpetualAtlanticVaultLP
contract balance can be easily filled with any amount of any tokens accidentally or as a part of an attack. So malicious users can control the RpdxV2Core.settle
function revert.
Manual review
Consider using a not so strict requirement or balanceOf
/_totalCollateral
synchronization.
DoS
#0 - c4-pre-sort
2023-09-09T09:54:25Z
bytes032 marked the issue as duplicate of #619
#1 - c4-pre-sort
2023-09-11T16:14:21Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T20:01:40Z
GalloDaSballo marked the issue as satisfactory
#3 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#4 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#5 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#6 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#7 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
🌟 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/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/core/RdpxV2Core.sol#L1001-L1003 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/core/RdpxV2Core.sol#L975-L990
The RdpxV2Core
contract functionality can be blocked as long as the contract WETH
balance is less than totalWethDelegated
. This can happen even without malicious activities.
The sync
function of the RdpxV2Core
contract has a special calculation for WETH
token:
1001 if (weth == reserveAsset[i].tokenAddress) { 1002 balance = balance - totalWethDelegated; 1003 } 1004 reserveAsset[i].tokenBalance = balance;
The totalWethDelegated
storage variable is increased at the addToDelegate
function and decreased only at the _bondWithDelegate
function. But the withdraw
function lets withdrawing delegated WETH
token without decreasing the totalWethDelegated
. A malicious user can easily increase totalWethDelegated
to any value (much more than weth.balanceOf
) by using a batch of the addToDelegate
and totalWethDelegated
functions to the protocol blocking due to error at the sync
function. The same can happen without malicious activities.
The sync
is used in the core functionality. So in case of error at the sync
due to underflow the many other functions will be also unavailable.
Manual review.
Consider decreasing totalWethDelegated
at the withdraw
function.
DoS
#0 - c4-pre-sort
2023-09-07T07:28:18Z
bytes032 marked the issue as duplicate of #2186
#1 - c4-pre-sort
2023-09-07T07:29:33Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T17:52:44Z
GalloDaSballo marked the issue as satisfactory
#3 - c4-judge
2023-10-20T17:55:32Z
GalloDaSballo changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-10-21T07:38:54Z
GalloDaSballo changed the severity to 3 (High Risk)
#5 - c4-judge
2023-10-21T07:39:55Z
GalloDaSballo marked the issue as partial-50
🌟 Selected for report: c3phas
Also found by: 0xgrbr, HHK, LeoS, QiuhaoLi, Sathish9098, __141345__, flacko, oakcobalt, pontifex
101.0486 USDC - $101.05
There are 2 instances of double checking in the RdpxDecayingBonds.mint
function. The hasRole(MINTER_ROLE, msg.sender)
check is the same as the onlyRole(MINTER_ROLE)
modifier. The _whenNotPaused
check is the same as the _beforeTokenTransfer
function, which is called from the _mint
function.
118 ) external onlyRole(MINTER_ROLE) { 119 _whenNotPaused(); 120 require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter");
Consider using unchecked calculation at the PerpetualAtlanticVaultLP.beforeWithdraw
function due the check at the line L288 (totalAvailableCollateral() = _totalCollateral - _activeCollateral
).
function beforeWithdraw(uint256 assets, uint256 /*shares*/) internal { require( assets <= totalAvailableCollateral(), "Not enough available assets to satisfy withdrawal" ); _totalCollateral -= assets; }
There are 4 variables which can be immutables:
69 address public rdpx; 72 address public rdpxV2Core;
46 IPerpetualAtlanticVault public perpetualAtlanticVault; 67 address public rdpx;
There are 2 variables which can be constants:
104 uint256 public roundingPrecision = 1e6;
104 uint256 public liquiditySlippageTolerance = 5e5; // 0.5%
The delegates
array can be internal because it has a public getters getDelegatesLength
and getDelegatePosition
at the RdpxV2Core
contract.
https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/core/RdpxV2Core.sol#L121
There are redundant multiplications and divisions by the DEFAULT_PRECISION
constant which have no effect at the RdpxV2Core
contract:
1169 rdpxRequired = 1170 ((RDPX_RATIO_PERCENTAGE - (bondDiscount / 2)) * 1171 _amount * 1172 DEFAULT_PRECISION) / 1173 (DEFAULT_PRECISION * rdpxPrice * 1e2); 1180 rdpxRequired = 1181 (RDPX_RATIO_PERCENTAGE * _amount * DEFAULT_PRECISION) / 1182 (DEFAULT_PRECISION * rdpxPrice * 1e2);
The PerpetualAtlanticVault.calculateFunding
calculates timeToExpiry
in a for-loop. But for any conditions the result of the calculation will be exactly fundingDuration
. This could be a potentially wrong calculations, the documentation says The APP contract follows a epoch system where each epoch is 1 week long. ... The funding is calculated based on the amount of options active for a given strike using BlackScholes with the duration as the start of the epoch until the end of the epoch.
So the result is correct.
Proof of concept: uint256 timeToExpiry = nextFundingPaymentTimestamp() - (genesis + ((latestFundingPaymentPointer - 1) * fundingDuration)) = genesis + latestFundingPaymentPointer * fundingDuration - genesis - (latestFundingPaymentPointer - 1) * fundingDuration = latestFundingPaymentPointer * fundingDuration - (latestFundingPaymentPointer - 1) * fundingDuration = latestFundingPaymentPointer * fundingDuration - latestFundingPaymentPointer * fundingDuration + 1 * fundingDuration = fundingDuration
.
This issue is in both the QA and GAS categories.
426 uint256 timeToExpiry = nextFundingPaymentTimestamp() - 427 (genesis + ((latestFundingPaymentPointer - 1) * fundingDuration));
There are three functions at the PerpetualAtlanticVaultLP
contract where a stack variable can be used to reduce storage readings and calculations.
function foo(uint256 value) public onlyPerpVault { uint256 newVariable = storageVariable + value; require( collateral.balanceOf(address(this)) >= newVariable, "Not enough collateral token was sent" ); storageVariable = newVariable; }
The strike
variable can be initialized before the _validate
call at the line L414:
413 for (uint256 i = 0; i < strikes.length; i++) { 414 _validate(optionsPerStrike[strikes[i]] > 0, 4); 415 _validate( 416 latestFundingPerStrike[strikes[i]] != latestFundingPaymentPointer, 417 5 418 ); 419 uint256 strike = strikes[i];
https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVault.sol#L413-L419
The nextFundingPaymentTimestamp()
call result can be cached before the if
at the line L464 because the new result is possible only after the latestFundingPaymentPointer
incrementing.
462 function updateFundingPaymentPointer() public { 463 while (block.timestamp >= nextFundingPaymentTimestamp()) { 464 if (lastUpdateTime < nextFundingPaymentTimestamp()) { 468 ? (nextFundingPaymentTimestamp() - fundingDuration) 471 lastUpdateTime = nextFundingPaymentTimestamp(); 475 (currentFundingRate * (nextFundingPaymentTimestamp() - startTime)) / 481 (currentFundingRate * (nextFundingPaymentTimestamp() - startTime)) / 487 ((currentFundingRate * (nextFundingPaymentTimestamp() - startTime)) / 491 } 492 493 latestFundingPaymentPointer += 1; 494 emit FundingPaymentPointerUpdated(latestFundingPaymentPointer); 495 } 496 }
The _amounts.length
value can be cached before the _validate
call at the line L827 of the RdpxV2Core
contract:
827 _validate(_amounts.length == _delegateIds.length, 3); 832 uint256[] memory delegateReceiptTokenAmounts = new uint256[]( 833 _amounts.length 834 ); 836 for (uint256 i = 0; i < _amounts.length; i++) {
There is a redundant call at the RdpxV2Core._transfer
function. The ignored return at the line L631 is exactly ownerOf(_bondId)
. It can be cached to avoid another calling to the IRdpxDecayingBonds(addresses.rdpxDecayingBonds)
contract.
631 (, uint256 expiry, uint256 amount) = IRdpxDecayingBonds( 632 addresses.rdpxDecayingBonds 633 ).bonds(_bondId); 638 IRdpxDecayingBonds(addresses.rdpxDecayingBonds).ownerOf(_bondId) == 639 msg.sender, 640 9 641 );
#0 - c4-pre-sort
2023-09-10T11:28:37Z
bytes032 marked the issue as sufficient quality report
#1 - GalloDaSballo
2023-10-10T18:45:06Z
L
NC
-3
L
I
NC
NC
L
L
4L 3NC - 3
#2 - c4-judge
2023-10-20T10:19:31Z
GalloDaSballo marked the issue as grade-b