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: 16/183
Findings: 6
Award: $529.70
🌟 Selected for report: 2
🚀 Solo Findings: 0
🌟 Selected for report: MrPotatoMagic
Also found by: 0xtankr, ArmedGoose, Egis_Security, Giorgio, KYP, Maroutis, NentoR, OMEN, Sabit, Shubham, SpicyMeatball, T1MOH, d3e4, dimulski, peanuts
261.0888 USDC - $261.09
The liquidate() function allows liquidators to burn DYAD on behalf of an DNft id and receive collateral in return.
The issue is that the current functionality only allows burning of the whole DYAD amount minted by the DNft id. This means that partial liquidations cannot be performed and prevents liquidators from liquidating DYAD minted by whales that hold huge positions in the system. Since the liquidations cannot be performed unless the liquidator can match upto the collateral deposited and DYAD minted by the whale, the system will be undercollaterized causing bad debt to accrue.
The effect of this issue will increase as more such positions exist in the system that cannot be liquidated by the liquidators.
In the liquidate() function below, we can see on Line 235 that when the burn() function is called on the DYAD token contract, it burns the whole minted DYAD instead of allowing the liquidator to supply a specific amount they can burn to improve the collateral ratio of the id and the overall health of the system.
But since this is not allowed, liquidators trying to liquidate whales, who have minted a huge amount of DYAD, would fail due to the position being extremely big and the inability of partially liquidate.
File: VaultManagerV2.sol 225: function liquidate( 226: uint id, 227: uint to 228: ) 229: external 230: isValidDNft(id) 231: isValidDNft(to) 232: { 233: uint cr = collatRatio(id); 234: if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh(); 235: dyad.burn(id, msg.sender, dyad.mintedDyad(address(this), id)); 236: 237: 238: uint cappedCr = cr < 1e18 ? 1e18 : cr; 239: uint liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD); 240: uint liquidationAssetShare = (liquidationEquityShare + 1e18).divWadDown(cappedCr); 241: 242: uint numberOfVaults = vaults[id].length(); 243: for (uint i = 0; i < numberOfVaults; i++) { 244: Vault vault = Vault(vaults[id].at(i)); 245: uint collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare); 246: 247: vault.move(id, to, collateral); 248: } 249: emit Liquidate(id, msg.sender, to); 250: }
Manual Review
Implement a mechanism to allow liquidators to partially liquidate positions. This would also require refactoring the collateral paid out to them based on the amount they cover.
Other
#0 - c4-pre-sort
2024-04-27T17:48:27Z
JustDravee marked the issue as high quality report
#1 - c4-pre-sort
2024-04-27T17:48:30Z
JustDravee marked the issue as primary issue
#2 - shafu0x
2024-04-29T23:46:40Z
hmm, but can't this be solved by flash loaning DYAD?
#3 - 0xMax1
2024-04-30T07:54:25Z
hmm, but can't this be solved by flash loaning DYAD?
Not if loan exceeds market liquidity.
Partial liquidations is a feature we should implement.
@shafu0x I suggest we label issue 1097 as sponsor confirmed
#4 - c4-judge
2024-05-05T13:29:39Z
koolexcrypto marked the issue as satisfactory
#5 - c4-judge
2024-05-08T08:56:17Z
koolexcrypto marked the issue as selected for report
🌟 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.0304 USDC - $0.03
Root cause: Function deposit() uses modifier isValidDNft() instead of isDNftOwner(), which allows anyone to call deposit() on behalf of any DNft id.
Impacts:
Let's understand the first impact:
User calls redeemDyad() (or withdraw() to directly withdraw collateral) to burn DYAD and withdraw their collateral asset.
Attacker frontruns the call with a 0 value deposit() call. This would set the idToBlockOfLastDeposit[id] to the current block.number. The call does not revert since 0 value transfers are allowed.
File: VaultManagerV2.sol 127: function deposit( 128: uint id, 129: address vault, 130: uint amount 131: ) 132: external 133: isValidDNft(id) 134: { 135: idToBlockOfLastDeposit[id] = block.number; 136: Vault _vault = Vault(vault); 137: _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); 138: _vault.deposit(id, amount); 139: }
File: VaultManagerV2.sol 143: function withdraw( 144: uint id, 145: address vault, 146: uint amount, 147: address to 148: ) 149: public 150: isDNftOwner(id) 151: { 152: if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); 153: uint dyadMinted = dyad.mintedDyad(address(this), id); 154: Vault _vault = Vault(vault); 155: uint value = amount * _vault.assetPrice() 156: * 1e18 157: / 10**_vault.oracle().decimals() 158: / 10**_vault.asset().decimals(); 159: 160: if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat(); 161: _vault.withdraw(id, to, amount); 162: if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); 163: }
Let's understand the second impact:
User tries to remove a vault by calling the remove() function.
Attacker frontruns the call by making a 1 wei collateral deposit through the deposit() function. This would increase the id2asset for the user in the vault.
File: VaultManagerV2.sol 127: function deposit( 128: uint id, 129: address vault, 130: uint amount 131: ) 132: external 133: isValidDNft(id) 134: { 135: idToBlockOfLastDeposit[id] = block.number; 136: Vault _vault = Vault(vault); 137: _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); 138: _vault.deposit(id, amount); 139: }
File: VaultManagerV2.sol 095: function remove( 096: uint id, 097: address vault 098: ) 099: external 100: isDNftOwner(id) 101: { 102: if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets(); 103: if (!vaults[id].remove(vault)) revert VaultNotAdded(); 104: emit Removed(id, vault); 105: }
Manual Review
Use modifier isDNftOwner() instead of isValidDNft() on function deposit().
Invalid Validation
#0 - c4-pre-sort
2024-04-27T11:34:33Z
JustDravee marked the issue as duplicate of #1103
#1 - c4-pre-sort
2024-04-27T11:34:41Z
JustDravee marked the issue as high quality report
#2 - c4-pre-sort
2024-04-27T11:45:55Z
JustDravee marked the issue as duplicate of #489
#3 - c4-judge
2024-05-05T20:38:09Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-05T21:14:21Z
koolexcrypto marked the issue as nullified
#5 - c4-judge
2024-05-05T21:14:29Z
koolexcrypto marked the issue as not nullified
#6 - c4-judge
2024-05-08T15:19:56Z
koolexcrypto marked the issue as not a duplicate
#7 - c4-judge
2024-05-08T15:20:00Z
koolexcrypto marked the issue as primary issue
#8 - c4-judge
2024-05-08T15:30:49Z
koolexcrypto marked the issue as selected for report
🌟 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
Judge has assessed an item in Issue #1288 as 2 risk. The relevant finding follows:
[L-03] withdraw() function does not consider kerosene value Kerosene value is not considered here in the check above the withdraw() call on the vault contract. This is problematic for kerosene vaults because getNonKeroseneValue() for users only using kerosene vaults would lead to revert due to 0 - non-zero amount occurring.
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();
}
#0 - c4-judge
2024-05-10T11:49:15Z
koolexcrypto changed the severity to 3 (High Risk)
#1 - c4-judge
2024-05-10T11:49:27Z
koolexcrypto marked the issue as duplicate of #397
#2 - c4-judge
2024-05-11T19:23:15Z
koolexcrypto marked the issue as satisfactory
🌟 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
Judge has assessed an item in Issue #1288 as 2 risk. The relevant finding follows:
[L-03] withdraw() function does not consider kerosene value Kerosene value is not considered here in the check above the withdraw() call on the vault contract. This is problematic for kerosene vaults because getNonKeroseneValue() for users only using kerosene vaults would lead to revert due to 0 - non-zero amount occurring.
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();
}
#0 - c4-judge
2024-05-13T11:42:59Z
koolexcrypto marked the issue as duplicate of #397
#1 - c4-judge
2024-05-13T11:43:19Z
koolexcrypto marked the issue as satisfactory
#2 - c4-judge
2024-05-13T18:33:04Z
koolexcrypto changed the severity to 3 (High Risk)
🌟 Selected for report: dimulski
Also found by: 0xleadwizard, 0xlemon, Aamir, Al-Qa-qa, AvantGard, Bauchibred, Cryptor, DarkTower, Egis_Security, Giorgio, Maroutis, MrPotatoMagic, OMEN, Ocean_Sky, Ryonen, SBSecurity, Sabit, SpicyMeatball, Stefanov, T1MOH, Tigerfrake, WildSniper, atoko, bhilare_, darksnow, fandonov, grearlake, iamandreiski, igdbase, pontifex, web3km, xiao
4.8719 USDC - $4.87
There is no incentive to liquidate low value positions such as 5$ USD worth of collateral because of gas costs on Ethereum (even worse in high gas scenarios). This causes the protocol to potentially accrue bad debt and become under-collaterized over time.
Liquidators will proceed to liquidate users if there is an incentive. If there isn't one, no one will call the liquidate() function.
For example, a user deposits 8 USD worth of collateral and mints 4 DYAD. Let's say the price of the collateral drops causing the value of go from 8 USD to 5 USD.
Now since the user has 5 USD worth of collateral and has 4 DYAD minted, the user is undercollateralized and must be liquidated in order to ensure that the protocol remains overcollateralized (150% of 4 DYAD = 6 USD minimum threshold required but value of collateral is 5 USD right now).
Because the value of the position is so low, after gas costs on Ethereum, liquidators will not make a profit liquidating this user. In the end, these low value positions will never get liquidated, leaving the protocol with bad debt and can even cause the protocol to be undercollateralized with enough small value accounts being underwater.
Manual Review
Consider allowing users to mint DYAD if their collateral value is past a certain threshold.
Error
#0 - c4-pre-sort
2024-04-27T17:31:05Z
JustDravee marked the issue as duplicate of #1258
#1 - c4-pre-sort
2024-04-29T09:16:47Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-03T14:07:47Z
koolexcrypto changed the severity to QA (Quality Assurance)
#3 - c4-judge
2024-05-12T09:33:28Z
koolexcrypto marked the issue as grade-c
#4 - c4-judge
2024-05-22T14:26:07Z
This previously downgraded issue has been upgraded by koolexcrypto
#5 - c4-judge
2024-05-28T16:52:33Z
koolexcrypto marked the issue as satisfactory
#6 - c4-judge
2024-05-28T20:06:34Z
koolexcrypto marked the issue as duplicate of #175
🌟 Selected for report: TheSavageTeddy
Also found by: 0x175, 0x486776, 0xnev, AamirMK, AlexCzm, ArmedGoose, BiasedMerc, CaeraDenoir, Egis_Security, Jorgect, KYP, MrPotatoMagic, PoeAudits, SBSecurity, SovaSlava, VAD37, adam-idarrha, alix40, carrotsmuggler, d_tony7470, dimulski, grearlake, josephdara, ljj, n0kto, okolicodes, sashik_eth, sil3th, turvy_fuzz
7.3512 USDC - $7.35
Root cause: Function deposit() uses modifier isValidDNft() instead of isDNftOwner(), which allows anyone to call deposit() on behalf of any DNft id.
Impacts:
Let's understand the first impact:
User calls redeemDyad() (or withdraw() to directly withdraw collateral) to burn DYAD and withdraw their collateral asset.
Attacker frontruns the call with a 0 value deposit() call. This would set the idToBlockOfLastDeposit[id] to the current block.number. The call does not revert since 0 value transfers are allowed.
File: VaultManagerV2.sol 127: function deposit( 128: uint id, 129: address vault, 130: uint amount 131: ) 132: external 133: isValidDNft(id) 134: { 135: idToBlockOfLastDeposit[id] = block.number; 136: Vault _vault = Vault(vault); 137: _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); 138: _vault.deposit(id, amount); 139: }
File: VaultManagerV2.sol 143: function withdraw( 144: uint id, 145: address vault, 146: uint amount, 147: address to 148: ) 149: public 150: isDNftOwner(id) 151: { 152: if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); 153: uint dyadMinted = dyad.mintedDyad(address(this), id); 154: Vault _vault = Vault(vault); 155: uint value = amount * _vault.assetPrice() 156: * 1e18 157: / 10**_vault.oracle().decimals() 158: / 10**_vault.asset().decimals(); 159: 160: if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat(); 161: _vault.withdraw(id, to, amount); 162: if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); 163: }
Let's understand the second impact:
User tries to remove a vault by calling the remove() function.
Attacker frontruns the call by making a 1 wei collateral deposit through the deposit() function. This would increase the id2asset for the user in the vault.
File: VaultManagerV2.sol 127: function deposit( 128: uint id, 129: address vault, 130: uint amount 131: ) 132: external 133: isValidDNft(id) 134: { 135: idToBlockOfLastDeposit[id] = block.number; 136: Vault _vault = Vault(vault); 137: _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); 138: _vault.deposit(id, amount); 139: }
File: VaultManagerV2.sol 095: function remove( 096: uint id, 097: address vault 098: ) 099: external 100: isDNftOwner(id) 101: { 102: if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets(); 103: if (!vaults[id].remove(vault)) revert VaultNotAdded(); 104: emit Removed(id, vault); 105: }
Manual Review
Use modifier isDNftOwner() instead of isValidDNft() on function deposit().
Invalid Validation
#0 - thebrittfactor
2024-05-29T13:29:42Z
For transparency, the judge has requested that issue #1001Â be duplicated, as it contains two issues they deemed should be judged separately.
#1 - c4-judge
2024-05-29T13:48:07Z
koolexcrypto marked the issue as duplicate of #118
#2 - c4-judge
2024-05-29T13:48:14Z
koolexcrypto marked the issue as satisfactory
#3 - koolexcrypto
2024-05-29T13:48:34Z
Split from #1001
#4 - c4-judge
2024-05-29T13:55:32Z
koolexcrypto changed the severity to 2 (Med Risk)
🌟 Selected for report: Pataroff
Also found by: Egis_Security, Evo, Jorgect, MrPotatoMagic, SBSecurity, T1MOH, carrotsmuggler, ljj
223.9474 USDC - $223.95
Judge has assessed an item in Issue #1288 as 2 risk. The relevant finding follows:
[L-04] Attacker can grief users from redeeming collateral The burnDyad() function allows anyone to burn DYAD on behalf of any id. This allows an attacker to burn DYAD and thus prevent users from possible redeeming their collateral back due to mintedDyad underflowing during redeem.
function burnDyad( uint id, uint amount ) external isValidDNft(id) { dyad.burn(id, msg.sender, amount); emit BurnDyad(id, amount, msg.sender); }
#0 - c4-judge
2024-05-10T12:02:07Z
koolexcrypto marked the issue as duplicate of #992
#1 - c4-judge
2024-05-11T12:15:46Z
koolexcrypto marked the issue as satisfactory
#2 - c4-judge
2024-05-28T10:29:45Z
koolexcrypto marked the issue as duplicate of #100