Dopex - HChang26'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: 22/189

Findings: 6

Award: $954.93

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L764 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L315 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L199

Vulnerability details

Impact

Attacker can deny the protocol from settle() put options. dpxETH/ETH will de-peg.

Proof of Concept

settle() is an admin only function, called when the price of rPDX falls below certain level. settle() from RdpxV2Core calls settle() on PerpetualAtlanticVault. The losses are compensated with WETH from PerpetualAtlanticVaultLP. rPDx is transferred to PerpetualAtlanticVaultLP. And internal accounting will be updated on PerpetualAtlanticVaultLP.

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

Let's take a look at subtractLoss(), it requires actual balance to be exactly _totalCollateral - loss. Or else the entire function call would revert.

function subtractLoss(uint256 loss) public onlyPerpVault {
    require(
      collateral.balanceOf(address(this)) == _totalCollateral - loss,
      "Not enough collateral was sent out"
    );
    _totalCollateral -= loss;
  }

Malicious user can send PerpetualAtlanticVaultLP as little as 1 wei to create a conflict between actual balance and internal accounting. None of the put options can be settled and deny the protocol from increasing proper amount of backing in WETH and lead to de-peg.

Tools Used

Manual Review

It is almost impossible for the actual balance to exactly match up with internal accounting.

function subtractLoss(uint256 loss) public onlyPerpVault {
    require(
-     collateral.balanceOf(address(this)) == _totalCollateral - loss,
+     collateral.balanceOf(address(this)) >= _totalCollateral - loss,
      "Not enough collateral was sent out"
    );
    _totalCollateral -= loss;
  }

Assessed type

DoS

#0 - c4-pre-sort

2023-09-09T10:00:15Z

bytes032 marked the issue as duplicate of #619

#1 - c4-pre-sort

2023-09-11T16:15:08Z

bytes032 marked the issue as sufficient quality report

#2 - c4-judge

2023-10-20T19:35:24Z

GalloDaSballo marked the issue as satisfactory

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/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L118 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L269 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L274 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L190 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L462

Vulnerability details

Impact

Deposit() in PerpetualAtlanticVaultLP will mint additional shares to users.

Proof of Concept

Let's take a look at deposit(). Sanity checks shares has to be greater than 0 by previewDeposit().

require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES");

The function will then trigger perpetualAtlanticVault.updateFunding(), which updates the states in PerpetualAtlanticVaultLP, specifically _totalCollateral is added with new proceeds.

function updateFunding() public { updateFundingPaymentPointer(); uint256 currentFundingRate = fundingRates[latestFundingPaymentPointer]; uint256 startTime = lastUpdateTime == 0 ? (nextFundingPaymentTimestamp() - fundingDuration) : lastUpdateTime; lastUpdateTime = block.timestamp; collateralToken.safeTransfer( addresses.perpetualAtlanticVaultLP, (currentFundingRate * (block.timestamp - startTime)) / 1e18 ); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).addProceeds( (currentFundingRate * (block.timestamp - startTime)) / 1e18 ); emit FundingPaid( msg.sender, ((currentFundingRate * (block.timestamp - startTime)) / 1e18), latestFundingPaymentPointer ); }
function addProceeds(uint256 proceeds) public onlyPerpVault { require( collateral.balanceOf(address(this)) >= _totalCollateral + proceeds, "Not enough collateral token was sent" ); _totalCollateral += proceeds; }

However, _totalCollateral was used to calculate shares in convertToShares() before the state changes.

function convertToShares( uint256 assets ) public view returns (uint256 shares) { uint256 supply = totalSupply; uint256 rdpxPriceInAlphaToken = perpetualAtlanticVault.getUnderlyingPrice(); uint256 totalVaultCollateral = totalCollateral() + ((_rdpxCollateral * rdpxPriceInAlphaToken) / 1e8); return supply == 0 ? assets : assets.mulDivDown(supply, totalVaultCollateral); }

Function proceeds to mint shares calculated before state changes. This will lead to users receiving extra shares.

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); }

Tools Used

Manual Review

Update all state changes before previewDeposit()

function deposit( uint256 assets, address receiver ) public virtual returns (uint256 shares) { // Check for rounding error since we round down in previewDeposit. + perpetualAtlanticVault.updateFunding(); - require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); - perpetualAtlanticVault.updateFunding(); + require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); //@audit check return value // 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

Invalid Validation

#0 - c4-pre-sort

2023-09-07T13:48:24Z

bytes032 marked the issue as duplicate of #867

#1 - c4-pre-sort

2023-09-11T09:06:45Z

bytes032 marked the issue as sufficient quality report

#2 - c4-judge

2023-10-20T19:26:40Z

GalloDaSballo marked the issue as satisfactory

Lines of code

https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L941 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L975 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L819 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L995 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L790 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1080

Vulnerability details

Impact

bondWithDelegate(), provideFunding() and lowerDepeg() can be denied by malicious users.

Proof of Concept

All the WETH in Rdpxv2core belong in the 2 following categories: reserveAsset[reservesIndex["WETH"]].tokenBalance and totalWethDelegated. Users who do not have both WETH and rDPX can provide WETH by addToDelegate(). toalWethDelegated is updated here:

IERC20WithBurn(weth).safeTransferFrom(msg.sender, address(this), _amount); Delegate memory delegatePosition = Delegate({ amount: _amount, fee: _fee, owner: msg.sender, activeCollateral: 0 }); delegates.push(delegatePosition); // add amount to total weth delegated totalWethDelegated += _amount;

However, when withdraw() sends back un-delegated WETH, totalWethDelegated is not properly deducted.

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; IERC20WithBurn(weth).safeTransfer(msg.sender, amountWithdrawn); emit LogDelegateWithdraw(delegateId, amountWithdrawn); }

This creates an issue whether malicious users or not. One can repeatedly flash loan addToDelegate() and withdraw() in one transaction, so totalWethDelegated = type(uint256).max. No other user can provide WETH to delegate. The protocol will be left with empty delegatePosition.

A different attack vector would be for the attacker to call sync() after flash loan, so reserveAsset[reservesIndex["WETH"]].tokenBalance can be reduced to 0. This would disable 2 admin only functions, lowerDepeg() and provideFunding() which can break the entire system. Considering the low gas fee on Arbitrum, this attack can be achieved with little to no cost.

function sync() external { for (uint256 i = 1; i < reserveAsset.length; i++) { uint256 balance = IERC20WithBurn(reserveAsset[i].tokenAddress).balanceOf( address(this) ); if (weth == reserveAsset[i].tokenAddress) { balance = balance - totalWethDelegated; } reserveAsset[i].tokenBalance = balance; } emit LogSync(); }

Add 2 following PoC to unit test:

address user1 = makeAddr("user1"); address user2 = makeAddr("user2"); function testAddToDelegatesDoS() public{ //@Note Mock token does not allow minting type(uint256).max //@Note totalWethDelegated on RdpxV2Core needs to be changed to uint128 to demonstrate this attack (same idea applies). //Fund the users weth.mint(user1, type(uint128).max); weth.mint(user2, 100e18); //check total WETH delegated before == 0 uint256 totalWethDelegatedBefore = rdpxV2Core.totalWethDelegated(); console.logUint(totalWethDelegatedBefore); //user1 flash loan add and withdraw in one transaction vm.startPrank(user1); weth.approve(address(rdpxV2Core), type(uint128).max); uint256 delegateId = rdpxV2Core.addToDelegate(type(uint128).max, 1e8); rdpxV2Core.withdraw(delegateId); vm.stopPrank(); //check totalWethDelegated after uint256 totalWethDelegatedAfter = rdpxV2Core.totalWethDelegated(); console.logUint(totalWethDelegatedAfter); //user2 tries to add WETH to delegate vm.startPrank(user2); weth.approve(address(rdpxV2Core), 100e18); vm.expectRevert(); rdpxV2Core.addToDelegate(100e18, 1e8); } function testDosProvideFunding() public { //@note add "address admin = makeAddr("admin");" in Setup.t.sol //@note add "rdpxV2Core.grantRole(rdpxV2Core.DEFAULT_ADMIN_ROLE(), admin);" in Setup.t.sol //Fund users weth.mint(user1, type(uint128).max); weth.mint(user2, type(uint128).max); rdpx.mint(user1, 100e18); //User1 calls bond so there is some WETH reserve vm.startPrank(user1); weth.approve(address(rdpxV2Core), 10e18); rdpx.approve(address(rdpxV2Core), 10e18); rdpxV2Core.bond(5e18, 0, user1); vm.stopPrank(); //Check WETH reserve ( ,uint256 wethBalance, ) = rdpxV2Core.getReserveTokenInfo("WETH"); console.logUint(wethBalance); //User2 flash loans and calls sync vm.startPrank(user2); weth.approve(address(rdpxV2Core), type(uint128).max); uint256 delegateId = rdpxV2Core.addToDelegate(1225000000000000000, 1e8); rdpxV2Core.withdraw(delegateId); rdpxV2Core.sync(); vm.stopPrank(); //Check WETH reserve after sync() ( ,uint256 wethBalanceAfterSync, ) = rdpxV2Core.getReserveTokenInfo("WETH"); console.logUint(wethBalanceAfterSync); //Admin tries to provide funding but should revert since WETH reserve is 0 vm.startPrank(admin); weth.approve(address(vault), type(uint128).max); vm.expectRevert(); rdpxV2Core.provideFunding(); vm.stopPrank(); }

Tools Used

Manual Review and Foundry

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-08T14:13:50Z

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:57:01Z

GalloDaSballo marked the issue as satisfactory

#3 - c4-judge

2023-10-21T07:38:54Z

GalloDaSballo changed the severity to 3 (High Risk)

Findings Information

🌟 Selected for report: said

Also found by: HChang26, peakbolt

Labels

bug
2 (Med Risk)
downgraded by judge
partial-50
edited-by-warden
duplicate-780

Awards

673.8793 USDC - $673.88

External Links

Lines of code

https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1156 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L471 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L255 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L502 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L462

Vulnerability details

Impact

Inconsistent premium calculations in bond() and bondWithDelegate(). This can cause discrepancy in assets required for bonding.

Proof of Concept

When putOptionsRequired is toggled to true, premium is calculated in bond() and bondWithDelegate() . Let's take a look at bond() (same issue in bondWithDelegate()): bond() calls calculateBondCost() in order to calculate rdpxRequired and wethRequired. premium is calculated from strike and timeToExpiry on PerpetualAtlanticVault. We want to focus specifically on timeToExpiry, since the state on PerpetualAtlanticVault has not changed yet.

uint256 strike = IPerpetualAtlanticVault(addresses.perpetualAtlanticVault) .roundUp(rdpxPrice - (rdpxPrice / 4)); // 25% below the current price uint256 timeToExpiry = IPerpetualAtlanticVault( addresses.perpetualAtlanticVault ).nextFundingPaymentTimestamp() - block.timestamp; if (putOptionsRequired) { wethRequired += IPerpetualAtlanticVault(addresses.perpetualAtlanticVault) .calculatePremium(strike, rdpxRequired, timeToExpiry, 0); }

User will then send in wethRequired. bond() will then trigger _purchaseOptions() which calls IPerpetualAtlanticVault.purchase(). updateFunding() is called:

function updateFunding() public { updateFundingPaymentPointer(); uint256 currentFundingRate = fundingRates[latestFundingPaymentPointer]; uint256 startTime = lastUpdateTime == 0 ? (nextFundingPaymentTimestamp() - fundingDuration) : lastUpdateTime; lastUpdateTime = block.timestamp; collateralToken.safeTransfer( addresses.perpetualAtlanticVaultLP, (currentFundingRate * (block.timestamp - startTime)) / 1e18 ); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).addProceeds( (currentFundingRate * (block.timestamp - startTime)) / 1e18 ); emit FundingPaid( msg.sender, ((currentFundingRate * (block.timestamp - startTime)) / 1e18), latestFundingPaymentPointer ); }

updateFundingPaymentPointer() will update latestFundingPaymentPointer and this value is used to calculate timeToExpiry.

while (block.timestamp >= nextFundingPaymentTimestamp()) { if (lastUpdateTime < nextFundingPaymentTimestamp()) { uint256 currentFundingRate = fundingRates[latestFundingPaymentPointer]; uint256 startTime = lastUpdateTime == 0 ? (nextFundingPaymentTimestamp() - fundingDuration) : lastUpdateTime; lastUpdateTime = nextFundingPaymentTimestamp(); collateralToken.safeTransfer( addresses.perpetualAtlanticVaultLP, (currentFundingRate * (nextFundingPaymentTimestamp() - startTime)) / 1e18 ); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP) .addProceeds( (currentFundingRate * (nextFundingPaymentTimestamp() - startTime)) / 1e18 ); emit FundingPaid( msg.sender, ((currentFundingRate * (nextFundingPaymentTimestamp() - startTime)) / 1e18), latestFundingPaymentPointer ); } latestFundingPaymentPointer += 1; emit FundingPaymentPointerUpdated(latestFundingPaymentPointer); }

premium calculated before latestFundingPaymentPointer state change will be different from after due to 2 different timeToExpiry values. This can result in discrepancy for both wethRequired and rdpxRequired.

Tools Used

Manual Review

Update all changes on PerpetualAtlanticVault before calculating premium.

function bond( uint256 _amount, uint256 rdpxBondId, address _to ) public returns (uint256 receiptTokenAmount) { _whenNotPaused(); // Validate amount _validate(_amount > 0, 4); + IPerpetualAtlanticVault( + addresses.perpetualAtlanticVault + ).updateFunding(); // Compute the bond cost (uint256 rdpxRequired, uint256 wethRequired) = calculateBondCost( _amount, rdpxBondId ); ... }
function bondWithDelegate( address _to, uint256[] memory _amounts, uint256[] memory _delegateIds, uint256 rdpxBondId ) public returns (uint256 receiptTokenAmount, uint256[] memory) { _whenNotPaused(); // Validate amount _validate(_amounts.length == _delegateIds.length, 3); + IPerpetualAtlanticVault( + addresses.perpetualAtlanticVault + ).updateFunding(); ... }

Assessed type

Context

#0 - c4-pre-sort

2023-09-09T06:13:04Z

bytes032 marked the issue as primary issue

#1 - c4-pre-sort

2023-09-12T06:33:35Z

bytes032 marked the issue as duplicate of #237

#2 - c4-pre-sort

2023-09-14T09:29:48Z

bytes032 marked the issue as not a duplicate

#3 - c4-pre-sort

2023-09-15T08:51:43Z

bytes032 marked the issue as duplicate of #761

#4 - c4-judge

2023-10-20T12:02:57Z

GalloDaSballo marked the issue as not a duplicate

#5 - GalloDaSballo

2023-10-20T12:03:07Z

TODO: Dups of Wrong Math for "normal" vs "with delegate"

#6 - c4-judge

2023-10-20T19:13:44Z

GalloDaSballo marked the issue as duplicate of #2187

#7 - c4-judge

2023-10-20T19:13:51Z

GalloDaSballo marked the issue as partial-50

#8 - GalloDaSballo

2023-10-20T19:13:56Z

Harder to understand than the main finding

#9 - c4-judge

2023-10-21T07:51:40Z

GalloDaSballo changed the severity to 2 (Med Risk)

Findings Information

🌟 Selected for report: gjaldon

Also found by: Evo, HChang26, QiuhaoLi, Toshii, Yanchuan, peakbolt, rvierdiiev, tapir

Labels

bug
2 (Med Risk)
satisfactory
edited-by-warden
duplicate-750

Awards

238.7514 USDC - $238.75

External Links

Lines of code

https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L145 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L218 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L315

Vulnerability details

Impact

In a black swan event where price of rPDX drops significantly. The protocol will settle() put options and increase WETH backing from PerpetualAtlanticVaultLP. Users who hold shares PerpetaulAtlanticVault LP Token will not be able to redeem().

Proof of Concept

Let's take a look at settle(). When rPDX price falls significantly, protocol increases backing by sending WETH to RdpxV2Core from PerpetaulAtlanticVault. In turn, rDPX is sent to PerpetaulAtlanticVault from RdpxV2Core.

// 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
    );

This can create a scenario where PerpetaulAtlanticVault only holds rPDX and no WETH. Users who try to redeem LP Token will be denied. Require statement checks assets received cannot be 0. However, _convertToAssets() will return 0 for assets for two reasons:

  1. When no WETH available.
  2. There is so little WETH in PerpetaulAtlanticVault, assets rounds down to 0.
function redeem(
    uint256 shares,
    address receiver,
    address owner
  ) public returns (uint256 assets, uint256 rdpxAmount) {
    perpetualAtlanticVault.updateFunding();

    if (msg.sender != owner) {
      uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals.

      if (allowed != type(uint256).max) {
        allowance[owner][msg.sender] = allowed - shares;
      }
    }
    (assets, rdpxAmount) = redeemPreview(shares);

    // Check for rounding error since we round down in previewRedeem.
    require(assets != 0, "ZERO_ASSETS");

    _rdpxCollateral -= rdpxAmount;

    beforeWithdraw(assets, shares);

    _burn(owner, shares);

    collateral.transfer(receiver, assets);

    IERC20WithBurn(rdpx).safeTransfer(receiver, rdpxAmount);

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

Tools Used

Manual Review

Consider living with some insignificant rounding error.

  function redeem(
    uint256 shares,
    address receiver,
    address owner
  ) public returns (uint256 assets, uint256 rdpxAmount) {
    perpetualAtlanticVault.updateFunding();

    if (msg.sender != owner) {
      uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals.

      if (allowed != type(uint256).max) {
        allowance[owner][msg.sender] = allowed - shares;
      }
    }
    (assets, rdpxAmount) = redeemPreview(shares);

    // Check for rounding error since we round down in previewRedeem.
-   require(assets != 0, "ZERO_ASSETS");
+   require(assets != 0 || rdpxAmount != 0, "ZERO_ASSETS/ZERO_RDPXAMOUNT");

    _rdpxCollateral -= rdpxAmount;

    beforeWithdraw(assets, shares);

    _burn(owner, shares);

    collateral.transfer(receiver, assets);

    IERC20WithBurn(rdpx).safeTransfer(receiver, rdpxAmount);

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

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-09-15T05:47:42Z

bytes032 marked the issue as duplicate of #750

#1 - c4-judge

2023-10-15T18:02:58Z

GalloDaSballo marked the issue as satisfactory

Awards

24.8267 USDC - $24.83

Labels

bug
2 (Med Risk)
satisfactory
sufficient quality report
edited-by-warden
duplicate-153

External Links

Lines of code

https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/reLP/ReLPContract.sol#L202

Vulnerability details

Impact

Dust amount of tokenB will be locked in ReLPContract.

Proof of Concept

reLP() can only be called by protocol admin only. Based on sponsor:

reLP basically remove a portion of the LP tokens owned by the AMO and swaps half the eth to rdpx and adds liquidity using the new ratio. After the swap the remaining assets gets sent back to core contract.

After adding liquidity back into UniswapV2, LP token and tokenA are sent back to rdpxV2Core. However, there is potentially unused tokenB. Based on current implementation, there is no way to retrieve unused tokenB in ReLPContract.

(, , uint256 lp) = IUniswapV2Router(addresses.ammRouter).addLiquidity( addresses.tokenA, addresses.tokenB, tokenAAmountOut, amountB / 2, 0, 0, address(this), block.timestamp + 10 ); // transfer the lp to the amo IERC20WithBurn(addresses.pair).safeTransfer(addresses.amo, lp); IUniV2LiquidityAmo(addresses.amo).sync(); // transfer rdpx to rdpxV2Core IERC20WithBurn(addresses.tokenA).safeTransfer( addresses.rdpxV2Core, IERC20WithBurn(addresses.tokenA).balanceOf(address(this)) ); IRdpxV2Core(addresses.rdpxV2Core).sync();

Tools Used

Manual Review

(, , uint256 lp) = IUniswapV2Router(addresses.ammRouter).addLiquidity( addresses.tokenA, addresses.tokenB, tokenAAmountOut, amountB / 2, 0, 0, address(this), block.timestamp + 10 ); // transfer the lp to the amo IERC20WithBurn(addresses.pair).safeTransfer(addresses.amo, lp); IUniV2LiquidityAmo(addresses.amo).sync(); // transfer rdpx to rdpxV2Core IERC20WithBurn(addresses.tokenA).safeTransfer( addresses.rdpxV2Core, IERC20WithBurn(addresses.tokenA).balanceOf(address(this)) ); + IERC20WithBurn(addresses.tokenB).safeTransfer( + addresses.rdpxV2Core, + IERC20WithBurn(addresses.tokenB).balanceOf(address(this)) + ); IRdpxV2Core(addresses.rdpxV2Core).sync();

Assessed type

Uniswap

#0 - c4-pre-sort

2023-09-09T17:30:50Z

bytes032 marked the issue as duplicate of #1286

#1 - c4-pre-sort

2023-09-11T15:38:19Z

bytes032 marked the issue as sufficient quality report

#2 - c4-judge

2023-10-18T12:14:29Z

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