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: 186/189
Findings: 1
Award: $0.01
🌟 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/PerpetualAtlanticVault.sol#L315-L369 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVault.sol#L372-L396
The functions settle & payFunding
are using the onlyRole(RDPXV2CORE_ROLE)
modifier so that only the RdpxV2Core contract can use these functions but additionally they are also using the _isEligibleSender
function that checks if a contract is whitelisted this is like having 2 sensitive roles for a contract which could lead to access control or Dos vulnerabilities in the contract.
if these functions are meant to be used only by the RdpxV2Core contract, then it's not necessary to add the _isEligibleSender
function because this requires adding the RdpxV2Core to the whitelisted contracts and if any other whitelisted contract (not the RdpxV2Core) is going to be used to call this functions then you should give this or these other contracts the RDPXV2CORE_ROLE
and this could lead to vulnerabilities in the contract.
there's no problem adding the RdpxV2Core contract to the whitelisted contracts, but if you have a whitelisted contract that you want it to be able to use only these 2 functions(settle & payFunding
) you will have to give the whitelisted contracts the RDPXV2CORE_ROLE
and this will also give access to this contract to the whole RDPXV2CORE_ROLE functionality of the protocol, so if this contract is malicious or get compromised, the whole RDPXV2CORE_ROLE will get compromised too.
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); }
function payFunding() external onlyRole(RDPXV2CORE_ROLE) returns (uint256) { _whenNotPaused(); //@audit porque usan este modifier si solo lo puede llamar el RDPXV2CORE_ROLE?? _isEligibleSender(); //@audit posible DOS? enviando minimo amout of eth o rdpx? _validate(totalActiveOptions == fundingPaymentsAccountedFor[latestFundingPaymentPointer], 6); collateralToken.safeTransferFrom( addresses.rdpxV2Core, address(this), totalFundingForEpoch[latestFundingPaymentPointer] ); _updateFundingRate(totalFundingForEpoch[latestFundingPaymentPointer]); emit PayFunding(msg.sender, totalFundingForEpoch[latestFundingPaymentPointer], latestFundingPaymentPointer); return (totalFundingForEpoch[latestFundingPaymentPointer]); }
Manual Code Review
It's recommended to always use more granular access control rules, guide you for the Principle of Least Privilege (POLP), and try to avoid these redundant privileges.
If only the RdpxV2Core contract can call these functions, then delete the _isEligibleSender();
lines in these functions or if you need that these functions can be called only by a whitelisted contract that is not necessary to have the RDPXV2CORE_ROLE
, then delete the onlyRole modifier.
Oracle
#0 - c4-pre-sort
2023-09-09T09:57:04Z
bytes032 marked the issue as duplicate of #619
#1 - c4-pre-sort
2023-09-11T16:14:31Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T20:01:46Z
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: 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#L199-L205 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/core/RdpxV2Core.sol#L764-L783 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVault.sol#L315-L369
An attacker can break the settlement mechanism of the protocol preventing being liquidated and preventing the settlement of all the options of an epoch for very low cost.
When the protocol has to settle the options the Admin calls the Settle
function in the RdpxV2Core contract, which then calls the Settle
function in the PerpetualAtlanticVault contract and this calls the subtractLoss
function in the PerpetualAtlanticVaultLP contract, the subtractLoss
function has to subtract the loss from the collateral (WETH) but first require the balance of the PerpetualAtlanticVaultLP contract has to be equal to totalCollateral - loss
, this open the door to an attacker to brick the settlement mechanism.
let's imagine that an epoch is about to finish and an attacker is about to be liquidated or he just wants to harm the protocol.
The attacker can transfer 1 wei of WETH to the PerpetualAtlanticVaultLP contract this will cause the require require(collateral.balanceOf(address(this)) == _totalCollateral - loss, "Not enough collateral was sent out");
in the subtractLoss function to revert the Tx for all the options the protocol is trying to settle.
function subtractLoss(uint256 loss) public onlyPerpVault { require(collateral.balanceOf(address(this)) == _totalCollateral - loss, "Not enough collateral was sent out"); _totalCollateral -= loss; }
To show bug I just added the following lines to the testSettle()
function in the unit.t.sol contracts of perp-vault and rdpxV2Core tests.
console.log("sending 1 wei to vaultLp..."); weth.transfer(address(vaultLp), 1);
and the result show the settle function reverted by the require of the subtractLoss function:
% forge test --match-test testSettle -vv [â †] Compiling... [â ’] Compiling 1 files with 0.8.19 [â ‘] Solc 0.8.19 finished in 2.37s Compiler run successful! Running 1 test for tests/perp-vault/Unit.t.sol:Unit [FAIL. Reason: Not enough collateral was sent out] testSettle() (gas: 890276) Logs: sending 1 wei to vaultLp... Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 956.60ms Running 1 test for tests/rdpxV2-core/Unit.t.sol:Unit [FAIL. Reason: Not enough collateral was sent out] testSettle() (gas: 2946563) Logs: sending 1 wei to vaultLp... Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 956.59ms Ran 2 test suites: 0 tests passed, 2 failed, 0 skipped (2 total tests) Failing tests: Encountered 1 failing test in tests/perp-vault/Unit.t.sol:Unit [FAIL. Reason: Not enough collateral was sent out] testSettle() (gas: 890276) Encountered 1 failing test in tests/rdpxV2-core/Unit.t.sol:Unit [FAIL. Reason: Not enough collateral was sent out] testSettle() (gas: 2946563) Encountered a total of 2 failing tests, 0 tests succeeded
Manual Code Review
One way to avoid this bug is simple change the == operator to >= in the require subtractLoss function, like this:
require(collateral.balanceOf(address(this)) >= _totalCollateral - loss, "Not enough collateral was sent out");
DoS
#0 - c4-pre-sort
2023-09-09T06:39:43Z
bytes032 marked the issue as duplicate of #619
#1 - c4-pre-sort
2023-09-11T16:14:08Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T20:01:43Z
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)
#8 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#9 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)