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: 96/183
Findings: 3
Award: $14.84
🌟 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
VaultManager::withdraw()
function is designed only for deposited exogenous collateralUser can not withdraw their deposited kerosene due to VaultManager::withdraw()
function is designed only for deposited exogenous collateral, as can be seen here:
function withdraw( uint id, address vault, uint amount, address to ) public isDNftOwner(id) { ... uint value = amount * _vault.assetPrice() * 1e18 / 10**_vault.oracle().decimals() / 10**_vault.asset().decimals(); ... }
As the kerosene-vault
doesn't have oracle()
field, this function would be reverted.
This would lead to user's deposited kerosene can never be withdrawed.
Here is the local test I used.
function setUp() public { helper = new DeployLocal().run(); aggregator = helper.aggregator; kerosene = helper.kerosene; nft = helper.nft; dyad = helper.dyad; licenser = helper.licenser; vaultKeroseneLicenser = helper.vaultKeroseneLicenser; vaultManager = helper.vaultManager; kerosineManager = helper.kerosineManager; unboundedKerosineVault = helper.unboundedKerosineVault; kerosineDenominator = helper.kerosineDenominator; ethVault = helper.ethVault; weth = helper.weth; kerosene.mint(MAINNET_OWNER, 1000000000 ether); vm.prank(randomAddress); uint256 id = mintNFT(randomAddress); vm.startPrank(MAINNET_OWNER); kerosene.transfer(randomAddress, 5e7 ether); vm.stopPrank(); vm.prank(address(vaultManager)); dyad.mint(id, randomAddress, 5e6 ether); } function mintNFT(address player) public returns (uint id){ vm.deal(player, 1000000 ether); // mintNFT id = nft.mintNft(player); } function testCantWithdrawKerosene() public { vm.prank(randomAddress); kerosene.transfer(user, 5e6 ether); vm.startPrank(user); uint id = mintNFT(user); kerosene.approve(address(vaultManager),5e6); vaultManager.deposit(id, address(unboundedKerosineVault), 5e6); vaultManager.addKerosene(id, address(unboundedKerosineVault)); vm.stopPrank(); vm.roll(block.number + 2); vm.startPrank(user); vaultManager.withdraw(id, address(unboundedKerosineVault), 5e6, user); vm.stopPrank(); }
The log is:
├─ [9251] VaultManagerV2::withdraw(1, UnboundedKerosineVault: [0x978e3286EB805934215a88694d80b09aDed68D90], 5000000 [5e6], user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D]) │ ├─ [557] DNft::ownerOf(1) [staticcall] │ │ └─ ← user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D] │ ├─ [2623] Dyad::mintedDyad(VaultManagerV2: [0xD718d5A27a29FF1cD22403426084bA0d479869a0], 1) [staticcall] │ │ └─ ← 0 │ ├─ [261] UnboundedKerosineVault::asset() [staticcall] │ │ └─ ← ERC20Mock: [0xA8452Ec99ce0C64f20701dB7dD3abDb607c00496] │ ├─ [271] ERC20Mock::decimals() [staticcall] │ │ └─ ← 18 │ ├─ [214] UnboundedKerosineVault::oracle() [staticcall] │ │ └─ ← "EvmError: Revert" │ └─ ← "EvmError: Revert" └─ ← "EvmError: Revert" Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 3.69ms Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)
Manual Review
Just implement another VaultManager::withdrawKerosene()
to handle the withdraw of kerosene
.
+ function withdrawKerosene(uint id, address vault, uint amount, address to) public isDNftOwner(id) { + if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); + Vault _vault = Vault(vault); + _vault.withdraw(id, to, amount); + if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); + }
Other
#0 - c4-pre-sort
2024-04-26T21:11:57Z
JustDravee marked the issue as duplicate of #1048
#1 - c4-pre-sort
2024-04-28T18:39:20Z
JustDravee marked the issue as duplicate of #830
#2 - c4-pre-sort
2024-04-29T08:44:26Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-11T20:05:08Z
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: 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
There's a scenario where the liquidator receives less than they are required to pay due to most of the collateral of the undercollateralized asset
being kerosene
.
This could result in plummeting prices of exogenous assets never being liquidated, posing a significant risk to the protocol. Such assets may remain undercollateralized indefinitely, potentially compromising the protocol's integrity.
Below is the test I wrote, assuming the price of WETH
drops:
contract LocalTest is Test, Parameters { // ... [snip] ... (Your setup code remains unchanged) function testNoIncentive() public { Deposit(user); Deposit(liquidator); // Let 1000 players deposit into the vault for (uint i = 3; i <= 1000; i++) { Deposit(address(uint160(i))); } // The price of WETH drops. aggregator.updateAnswer(1850e8); /////////////////////////// console.log("Collateral Ratio of 0: ", vaultManager.collatRatio(0)); console.log("Minted Dyad before (0): ", dyad.mintedDyad(address(vaultManager), 0) / 1e18); console.log("Minted Dyad before (1): ", dyad.mintedDyad(address(vaultManager), 1) / 1e18); console.log("Total kerosene value before (0): ", vaultManager.getKeroseneValue(0) / 1e18); console.log("Total kerosene value before (1): ", vaultManager.getKeroseneValue(1) / 1e18); console.log("Total non-kerosene value before (0): ", vaultManager.getNonKeroseneValue(0) / 1e18); console.log("Total non-kerosene value before (1): ", vaultManager.getNonKeroseneValue(1) / 1e18); console.log("Total asset before (0): ", vaultManager.getTotalUsdValue(0) / 1e18); console.log("Total asset before (1): ", vaultManager.getTotalUsdValue(1) / 1e18); vm.startPrank(liquidator); vaultManager.liquidate(0, 1); vm.stopPrank(); console.log(); console.log("Minted Dyad after (0): ", dyad.mintedDyad(address(vaultManager), 0) / 1e18); console.log("Minted Dyad after (1): ", dyad.mintedDyad(address(vaultManager), 1) / 1e18); console.log("Total kerosene value after (0): ", vaultManager.getKeroseneValue(0) / 1e18); console.log("Total kerosene value after (1): ", vaultManager.getKeroseneValue(1) / 1e18); console.log("Total non-kerosene value after (0): ", vaultManager.getNonKeroseneValue(0) / 1e18); console.log("Total non-kerosene value after (1): ", vaultManager.getNonKeroseneValue(1) / 1e18); console.log("Total asset after (0): ", vaultManager.getTotalUsdValue(0) / 1e18); console.log("Total asset after (1): ", vaultManager.getTotalUsdValue(1) / 1e18); } }
The logs are as follows:
Collateral Ratio of 0: 1466666666666666666 Minted Dyad before (0): 150000 Minted Dyad before (1): 150000 Total kerosene value before (0): 35000 Total kerosene value before (0): 35000 Total non-kerosene value before (0): 185000 Total non-kerosene value before (1): 185000 Total asset before (0): 220000 Total asset before (1): 220000 Minted Dyad after (0): 0 Minted Dyad after (1): 150000 Total kerosene value after (0): 35150 Total kerosene value after (1): 35150 Total non-kerosene value after (0): 47090 Total non-kerosene value after (1): 322909 Total asset after (0): 82240 Total asset after (1): 358059
Assuming the price of WETH
initially is 3000, then drops to 1850, it can be seen that liquidator id(1)
liquidates the asset of id(0)
with 150000 of DYAD
burned but only receives 358059 - 220000 = 138059.
Manual Review
To mitigate this issue, we can consider allowing kerosene as a reward to the liquidator. There's no harm in this as the awarded kerosene does not impact its price.
function liquidate( uint id, uint to ) external isValidDNft(id) isValidDNft(to) { ... + 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); + } }
Other
#0 - c4-pre-sort
2024-04-28T10:25:31Z
JustDravee marked the issue as duplicate of #128
#1 - c4-pre-sort
2024-04-29T09:07:44Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:39:31Z
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
VaultManagerV2::add
and VaultManagerV2::addKerosene
Do Not Function Correctly, Resulting in Incorrect Calculation of VaultManagerV2::collatRatio
The VaultManagerV2::add
function is intended to add only wethVault
and wstethVault
. However, since the vaultLicenser
also allows for unboundedKeroseneVault
, the function cannot operate as designed. Additionally, the VaultManagerV2::addKerosene
function checks for a valid vault
using keroseneManager
. However, the purpose of keroseneManager
is to manage the exogenous vault
s that affect the ERC20::Kerosene
price.
This issue can lead to an incorrect calculation of VaultManagerV2::collatRatio
. Vaults such as wethVault
can be added to both the vaults
and vaultsKerosene
mappings, resulting in collateral being counted twice. This incorrect collateral calculation can lead to undercollateralized assets needing to be liquidated but unable to be.
Below is the test I added to ./test/fork/v2.t.sol
, where player
is a randomly generated address:
... + address public player = makeAddr("player"); ... + function testcanAddWETHVaultToBothVaultsManager() public { + vm.deal(player, 1000 ether); + DNft nft = DNft(MAINNET_DNFT); + vm.prank(player); + uint id = nft.mintNft{value: 100 ether}(player); + vm.startPrank(player); + WETH(payable(MAINNET_WETH)).deposit{value: 1 ether}(); + WETH(payable(MAINNET_WETH)).approve(address(contracts.vaultManager), 1 ether); + contracts.vaultManager.deposit(id, address(contracts.ethVault), 1 ether); + vaultManager.add(id, address(contracts.ethVault)); + vaultManager.addKerosene(id, address(contracts.ethVault)); + console.log("Total value of nonKeroseneVault is: ", vaultManager.getNonKeroseneValue(id)); + console.log("Total value of keroseneVault is: ", vaultManager.getKeroseneValue(id)); + console.log("Total asset of NFT is: ", vaultManager.getTotalUsdValue(id)); + vm.stopPrank(); + }
The logs are as follows:
Total value of nonKeroseneVault is: 3229060729920000000000 Total value of keroseneVault is: 3229060729920000000000 Total asset of NFT is: 6458121459840000000000
As observed, the collateral is counted twice.
Manual Review
To address this issue, we should implement two separate licensers: one for non-Kerosene-vault
and another for Kerosene-vault
.
## Assessed type Other
#0 - c4-pre-sort
2024-04-27T17:10:15Z
JustDravee marked the issue as duplicate of #70
#1 - c4-pre-sort
2024-04-29T12:03:32Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:58:31Z
koolexcrypto marked the issue as satisfactory