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: 29/183
Findings: 7
Award: $395.74
π Selected for report: 1
π 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/49fb7174576e5147dc73d3222d16954aa641a2e0/src/core/VaultManagerV2.sol#L250-L286 https://github.com/code-423n4/2024-04-dyad/blob/49fb7174576e5147dc73d3222d16954aa641a2e0/script/deploy/Deploy.V2.s.sol#L64-L65 https://github.com/code-423n4/2024-04-dyad/blob/49fb7174576e5147dc73d3222d16954aa641a2e0/script/deploy/Deploy.V2.s.sol#L95
DYAD protocol allow users to deposit collateral and mint a dollar-pegged asset (DYAD) against it. Two types of vaults are proposed:
The issue is that right now, the deploy script declare WETH and wstETH vaults as both being exogenous and kerosine vaults. Also, unboundedKerosineVault
is licensed in Licenser
while it should be in KerosineManager
This completely mess-up the collatral accounting.
File: script/deploy/Deploy.V2.s.sol 61: 62: KerosineManager kerosineManager = new KerosineManager(); 63: 64:β kerosineManager.add(address(ethVault)); //<@audit: should not be added in KerosineManager 65:β kerosineManager.add(address(wstEth)); //...
File: script/deploy/Deploy.V2.s.sol 93: vaultLicenser.add(address(ethVault)); 94: vaultLicenser.add(address(wstEth)); 95:β vaultLicenser.add(address(unboundedKerosineVault)); //<@audit: should be added in KerosineManager, not Licenser 96: // vaultLicenser.add(address(boundedKerosineVault));
This also make it possible for a user to add these vaults both to their vaults
and vaultsKerosene
enumerableSet.
Which means that when collateral ratio will be estimated, user WETH
and wstETH
deposit will be counted as Kerosene and non-Kerosine value, effectively double-counting this collateral value:
File: src/core/VaultManagerV2.sol 230: function collatRatio( 231: uint id 232: ) 233: public 234: view 235: returns (uint) { 236: uint _dyad = dyad.mintedDyad(address(this), id); 237: if (_dyad == 0) return type(uint).max; 238: return getTotalUsdValue(id).divWadDown(_dyad); 239: } 240: 241: function getTotalUsdValue( 242: uint id 243: ) 244: public 245: view 246: returns (uint) { 247: return getNonKeroseneValue(id) + getKeroseneValue(id); //<@audit: actual deployment script will cause issue here 248: }
Loss of funds, user will be able to inflate their collateral value and thus mint more DYAD than system should allow
Manual review
The following diff will not be sufficient to make the whole system function correctly, as there are an issue with how Kerosine price calculate TVL.
Right now, the KerosineManager vaults
set holds two functions:
These two functions have no link, and shouldn't be mixed in the same set. Sponsor also refactor this part and separate this as two sets.
File: script/deploy/Deploy.V2.s.sol:64 KerosineManager kerosineManager = new KerosineManager(); - kerosineManager.add(address(ethVault)); - kerosineManager.add(address(wstEth)); vaultManager.setKeroseneManager(kerosineManager); kerosineManager.transferOwnership(MAINNET_OWNER); UnboundedKerosineVault unboundedKerosineVault = new UnboundedKerosineVault( vaultManager, Kerosine(MAINNET_KEROSENE), Dyad (MAINNET_DYAD), kerosineManager ); BoundedKerosineVault boundedKerosineVault = new BoundedKerosineVault( vaultManager, Kerosine(MAINNET_KEROSENE), kerosineManager ); KerosineDenominator kerosineDenominator = new KerosineDenominator( Kerosine(MAINNET_KEROSENE) ); unboundedKerosineVault.setDenominator(kerosineDenominator); unboundedKerosineVault.transferOwnership(MAINNET_OWNER); boundedKerosineVault. transferOwnership(MAINNET_OWNER); vaultLicenser.add(address(ethVault)); vaultLicenser.add(address(wstEth)); - vaultLicenser.add(address(unboundedKerosineVault)); + kerosineManager.add(address(unboundedKerosineVault)); - // vaultLicenser.add(address(boundedKerosineVault)); + kerosineManager.add(address(boundedKerosineVault));
Context
#0 - c4-pre-sort
2024-04-28T06:57:41Z
JustDravee marked the issue as duplicate of #152
#1 - c4-pre-sort
2024-04-28T06:58:34Z
JustDravee marked the issue as not a duplicate
#2 - c4-pre-sort
2024-04-28T06:58:38Z
JustDravee marked the issue as primary issue
#3 - c4-pre-sort
2024-04-28T07:04:10Z
JustDravee marked the issue as high quality report
#4 - shafu0x
2024-04-30T11:27:47Z
Unfortunately the role of the KeroseneManager
is completely misunderstood here.
#5 - c4-judge
2024-05-04T09:46:33Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#6 - InfectedIsm
2024-05-15T22:00:37Z
Hi @koolexcrypto, I don't understand the sponsor comment here. This is a matter of fact that the actual deployment script will cause collateral to be double counted. The role of KerosineManager is not documented, not correctly implemented, and don't have anything to do with this finding.
#7 - McCoady
2024-05-16T01:06:36Z
Agreeing with the above, the "correct" implementation of KeroseneManager
is that it will be a list of valid exogenous collateral sources (WETH, WSTETH), while VaultLicenser
will include both exogenous(ETH) and endogenous(Kerosene) collateral sources. This means that a user would be able to successfully add their WETH/WSTETH vaults by calling add
and addKerosene
creating the double spend:
function add( uint id, address vault ) external isDNftOwner(id) { if (vaults[id].length() >= MAX_VAULTS) revert TooManyVaults(); > if (!vaultLicenser.isLicensed(vault)) revert VaultNotLicensed(); // NOTE: WETH/WSTETH will not revert here if (!vaults[id].add(vault)) revert VaultAlreadyAdded(); emit Added(id, vault); } function addKerosene( uint id, address vault ) external isDNftOwner(id) { if (vaultsKerosene[id].length() >= MAX_VAULTS_KEROSENE) revert TooManyVaults(); > if (!keroseneManager.isLicensed(vault)) revert VaultNotLicensed(); // NOTE: WETH/WSETH will not revert here if (!vaultsKerosene[id].add(vault)) revert VaultAlreadyAdded(); emit Added(id, vault); }
#8 - AtanasDimulski
2024-05-16T06:46:42Z
I agree with the above comments and believe this issue should be judged as a valid high, due to the fact that the Deploy.V2.s.sol file is in the scope of this audit. And the deployment steps in it, are exactly what makes this issue possible and so detrimental for the protocol.
#9 - T1MOH593
2024-05-16T11:11:02Z
Maybe it's duplicate of #70
#10 - Al-Qa-qa
2024-05-17T04:21:32Z
From what the Deployment Script was written, NonKerosine
holds all Collaterals and Kerosine
holds Kerosine collaterals only.
IDK who such a thing was not documented, I said that the deployment is true for this point. which makes me miss more than one issue as I thought NonKerosineVaults
hold all vaults.
If This is a deployment script error, then it should be labeled as HIGH as.
And for issue #70 it is not a duplicate for it as it is a different issue, with different line, different impact, different root cause, and different mitigation.
#11 - ych18
2024-05-17T07:52:12Z
Many warden pointed to this issue. The error is in the deployment script as ethVault
& wstEth
are both licensed to KeroseneManager and to Licenser. This would make an attacker to add both of this vault as a normal vault and KeroseneVault. As a result, the rule of 150% collateral is no more respected.
Other similar issue :
#1130
#124
#12 - Emedudu
2024-05-17T11:31:52Z
Here are similar issues: #1142 #1130 #464 #243 #220 #124
#13 - Maroutis
2024-05-17T19:05:39Z
I believe the assumption made by this submission that the script is faulty is wrong. The mitigation given is also wrong and would lead to problems. However, there is an issue. Explained in #1133.
#14 - InfectedIsm
2024-05-28T15:10:48Z
Hi @koolexcrypto, I see my submission #966 has been updated to unsatisfactory
based on @Maroutis comments, which made multiple wrong assumptions about my submission.
Marouatis: The assuption made by #966 is wrong because the script is correct and the double licensing is necessary. The root cause is in the design due to how the vaults are added in the keroseneManager (for this reason I dont think this should be a dup of that issue). Unlike #966, this submission clearly explains that the current deployment is not faulty as it is necessary to have the double licensing, due to how the contracts are designed. However, it leads to two major issues (check Proof of Concept). The first POC highlights the fact that double counting of collateral is possible due to design.
This is what I say in my "Recommended Mitigation Steps" section. The remediation I proposed (which shouldn't :
The following diff will not be sufficient to make the whole system function correctly, as there are an issue with how Kerosine price calculate TVL. Right now, the KerosineManager vaults set holds two functions: list licensed kerosine vault list assets that are part of the TVL These two functions have no link, and shouldn't be mixed in the same set.
Marouatis: The mitigation given by #966 is wrong also, because not licensing normal vaults via
KeroseneManager
will not allowUnboundedKerosineVault
to use these vaults for the price calculations of the kerosene asset. The logical solution here is to slightly alter the design which is what was proposed in this submission.
Same here,the mitigation I have described clearly states that changing the deployment script will not be sufficient and that the sponsor will have to separate the two functions:
The following diff will not be sufficient to make the whole system function correctly, as there are an issue with how Kerosine price calculate TVL. [...] These two functions have no link, and shouldn't be mixed in the same set. Sponsor should also refactor this part and separate this as two sets.
This is in fact the exact same thing as described by Marouatis :
The design of the licensing part needs to be rethinked. The issue here is that the vaults mapping of the KerosineManager contract which is constructed via the method KerosineManager::add is the same mapping that is used by the UnboundedKerosineVault::assetPrice function. You can consider creating two separate mappings
We all agree to say that the current architecture is highly flawed concerning the accounting.
My submission is correct by saying that adding weth and wstEth to both kerosineManager
and Licenser
cause a double counting issue.
There are many way to solve the problem, and a submission shouldn't be invalidated on that sole purpose. In fact, the mitigation section do not interfer with the issue validity as per C4 rules, but only can impact the partial scoring. But again, my remediation clearly states that updating the deployment script will not be sufficient, and that a refactor of the functions of "listing licensed vault" and "counting TVL" should be separated.
#15 - koolexcrypto
2024-05-28T15:25:43Z
Thanks everyone for your input.
The deployment script has no problems. This issue is still valid since it is pointing to the same problem, might consider a partial credit though.
@InfectedIsm
my submission https://github.com/code-423n4/2024-04-dyad-findings/issues/966 has been updated to unsatisfactory based on @Maroutis https://github.com/code-423n4/2024-04-dyad-findings/issues/1133#issuecomment-2116296514,
This was already unsatisfactory, I just didn't change it yet. Please give some time, as I'm working on synchronising multiple issues at the same time.
#16 - c4-judge
2024-05-28T15:26:21Z
koolexcrypto removed the grade
#17 - c4-judge
2024-05-28T20:15:19Z
koolexcrypto marked the issue as satisfactory
#18 - c4-judge
2024-05-29T11:20:26Z
koolexcrypto marked the issue as duplicate of #1133
π Selected for report: Limbooo
Also found by: 0xabhay, 0xleadwizard, AM, ArmedGoose, Evo, HChang26, Infect3d, Jorgect, MiniGlome, SpicyMeatball, TheSchnilch, ahmedaghadi, favelanky, pontifex
238.0297 USDC - $238.03
To prevent users from exploiting oracle updates for arbitrage, a safeguard has been implemented. This safeguard makes it impossible to deposit and then withdraw in the same block number.
But there 3 issues how this is implemented in VaultManagerV2::deposit
:
This make it possible for a malicious user to call deposit
with a empty dummy vault, which will lock withdrawal for the victim for the present block for all vaults
Even with no dummy vault, a malicious user could deposit dust amount to any of the victim vaults to prevent him from withdrawing in same block
Possibility to prevent user from withdrawing
File: src/core/VaultManagerV2.sol 119: function deposit( 120: uint id, 121: address vault, 122: uint amount 123: ) 124: external 125:β isValidDNft(id) //<(1): missing isLicensed(vault) modifier 126: { 127:β idToBlockOfLastDeposit[id] = block.number; //<(3): locking withdrawal for all vaults for this id 128: Vault _vault = Vault(vault); 129: _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); 130: _vault.deposit(id, amount); 131: } 132: 133: /// @inheritdoc IVaultManager 134: function withdraw( 135: uint id, 136: address vault, 137: uint amount, 138: address to 139: ) 140: public 141: isDNftOwner(id) 142: { 143:β if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); //<(3) 144: uint dyadMinted = dyad.mintedDyad(address(this), id); 145: Vault _vault = Vault(vault); 146: uint value = amount * _vault.assetPrice() 147: * 1e18 148: / 10**_vault.oracle().decimals() 149: / 10**_vault.asset().decimals(); 150: if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat(); 151: _vault.withdraw(id, to, amount); 152: if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); 153: }
manual review
hasVault
which might be even better)mapping(address owner => address) allowed
+ modifier isAllowed(address)
): src/core/VaultManagerV2.sol:119 + mapping(address owner => address) allowed; function deposit( uint id, address vault, uint amount ) external isValidDNft(id) + isLicensed(vault) + isAllowed(msg.sender) { idToBlockOfLastDeposit[id] = block.number; Vault _vault = Vault(vault); _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); _vault.deposit(id, amount); }
DoS
#0 - c4-pre-sort
2024-04-27T11:34:24Z
JustDravee marked the issue as duplicate of #1103
#1 - c4-pre-sort
2024-04-27T11:45:54Z
JustDravee marked the issue as duplicate of #489
#2 - c4-pre-sort
2024-04-29T09:30:07Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-05T20:38:09Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-05T21:14:39Z
koolexcrypto marked the issue as nullified
#5 - c4-judge
2024-05-05T21:14:47Z
koolexcrypto marked the issue as not nullified
#6 - c4-judge
2024-05-05T21:15:05Z
koolexcrypto marked the issue as not a duplicate
#7 - c4-judge
2024-05-05T21:15:17Z
koolexcrypto marked the issue as duplicate of #1266
#8 - c4-judge
2024-05-11T12:18:49Z
koolexcrypto marked the issue as satisfactory
#9 - c4-judge
2024-05-28T19:12:54Z
koolexcrypto marked the issue as duplicate of #930
π 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
In order to withdraw assets from a vault, the collateral ratio (CR) of the position must be sufficient :
if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow();
The same checks are made in liquidate
and mintDyad
The thing is, VaultManagerV2::collatRatio
calls VaultManagerV2::getTotalUsdValue
, which calls VaultManagerV2::getKeroseneValue
which calls KerosineVault::getUsdValue
ultimately calling UnboundedKerosineVault::assetPrice()
File: src/core/Vault.kerosine.unbounded.sol 51: function assetPrice() 52: public 53: view 54: override 55: returns (uint) { 56: uint tvl; 57: address[] memory vaults = kerosineManager.getVaults(); 58: uint numberOfVaults = vaults.length; 59: for (uint i = 0; i < numberOfVaults; i++) { 60: Vault vault = Vault(vaults[i]); 61: tvl += vault.asset().balanceOf(address(vault)) 62: * vault.assetPrice() * 1e18 63: / (10**vault.asset().decimals()) 64: / (10**vault.oracle().decimals()); 65: } 66: uint numerator = tvl - dyad.totalSupply(); 67: uint denominator = kerosineDenominator.denominator(); 68: return numerator * 1e8 / denominator; 69: }
But neither this contract, or KerosineVault
(which it inherit from) has an oracle
getter to call.
Functions calling collatRatio()
are: liquidate
, withdraw
and mintDyad
This means user depositing Kerosine will see all their asset stuck in the vault.
But not only that, positions cannot be liquidated until the problem is solved (by redeploying a new KerosineVault with an oracle)
Manual review
Add an oracle to the KerosineVault
(or UnboundedKerosineVault
)
abstract contract KerosineVault is IVault, Owned(msg.sender) { using SafeTransferLib for ERC20; IVaultManager public immutable vaultManager; ERC20 public immutable asset; KerosineManager public immutable kerosineManager; //@audit unused state variable + IAggregatorV3 public immutable oracle; mapping(uint => uint) public id2asset;
DoS
#0 - c4-pre-sort
2024-04-26T21:35:52Z
JustDravee marked the issue as duplicate of #1048
#1 - c4-pre-sort
2024-04-28T18:39:21Z
JustDravee marked the issue as duplicate of #830
#2 - c4-pre-sort
2024-04-29T08:44:46Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-11T20:05:12Z
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
DYAD protocol allow users to deposit collateral and mint a dollar-pegged asset (DYAD) against it. Two types of vaults are proposed:
Minting of DYAD tokens require a minimum total (exo + endo) collateralization ratio >150%, and a minimum exo CR100%.
So this assertion should always be verified under good conditions (i.e prices do not changes to quickly): tvl > dyad.totalSupply()
But this is not a realistic that can hold forever.
File: src/core/Vault.kerosine.unbounded.sol 50: function assetPrice() 51: public 52: view 53: override 54: returns (uint) { 55: uint tvl; 56: address[] memory vaults = kerosineManager.getVaults(); 57: uint numberOfVaults = vaults.length; 58: for (uint i = 0; i < numberOfVaults; i++) { 59: Vault vault = Vault(vaults[i]); 60: tvl += vault.asset().balanceOf(address(vault)) 61: * vault.assetPrice() * 1e18 62: / (10**vault.asset().decimals()) 63: / (10**vault.oracle().decimals()); 64: } 65:β uint numerator = tvl - dyad.totalSupply();//<@audit: this can underflow 66: uint denominator = kerosineDenominator.denominator(); 67: return numerator * 1e8 / denominator; 68: } 69: }
But if multiple vaults, or a major vault sees its asset price plummet, this relationship will change.
In that case, the substraction will underflow resulting in a revert of assetPrice
The issue is that UnboundedKerosineVault::assetPrice
is called by VaultManagerV2::collatRatio
>> VaultManagerV2::getTotalUsdValue
>> VaultManagerV2::getNonKeroseneValue
And VaultManagerV2::collatRatio
is called by VaultManagerV2::liquidate
, which is the mechanism expected to clean up bad positions.
File: src/core/VaultManagerV2.sol 205: function liquidate( 206: uint id, //The ID of the dNFT to be liquidated. 207: uint to //The address where the collateral will be sent 208: ) 209: external 210: isValidDNft(id) 211: isValidDNft(to) 212: { 213:β uint cr = collatRatio(id); //<@audit: this can revert if TVL < dyad.totalSupply() 214: if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh(); 215: dyad.burn(id, msg.sender, dyad.mintedDyad(address(this), id)); 216:
If total TVL drops below the minimum CR of the protocol, liquidation will revert, preventing protocol to recover.
Manuel review
Mitigating that issue is difficult as the issue is associated to how Kerosine price is evaluated. This require to refactor how the token is priced, in case of such cases, but this surely shouldn't revert when called during liquidations. Collateralization ratio should also probably be revised to higher values to absorb unexpected dumps in value.
DoS
#0 - c4-pre-sort
2024-04-28T19:54:15Z
JustDravee marked the issue as duplicate of #224
#1 - c4-pre-sort
2024-04-29T09:04:42Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-08T08:31:59Z
koolexcrypto marked the issue as duplicate of #308
#3 - c4-judge
2024-05-11T20:09:48Z
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
22.478 USDC - $22.48
Right now there are no incentives to liquidate a position with a CR<1, as the liquidator will have to burn the full borrowed amount, and will get the full collateral.
But a CR<1 means collateral is worth less than borrowed amount.
So this is a clear loss for the liquidator, meaning no-one will liquidate the position.
File: src/core/VaultManagerV2.sol 205: function liquidate( 206: uint id, //The ID of the dNFT to be liquidated. 207: uint to //The address where the collateral will be sent 208: ) ...: // ... some code ... 215:β dyad.burn(id, msg.sender, dyad.mintedDyad(address(this), id)); //<@audit: caller need to burn full borrowed amount 216: 217: uint cappedCr = cr < 1e18 ? 1e18 : cr; /// == max(1e18, cr) 218: uint liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD); /// if cr<1, this is equal 0 219: uint liquidationAssetShare = (liquidationEquityShare + 1e18).divWadDown(cappedCr); /// if cr<1, this is equal 1e18 220: 221: uint numberOfVaults = vaults[id].length(); 222: for (uint i = 0; i < numberOfVaults; i++) { 223: Vault vault = Vault(vaults[id].at(i)); 224: uint collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare); 225: vault.move(id, to, collateral); 226: } 227: emit Liquidate(id, msg.sender, to); 228: }
If CR<1, position will not be liquidated, protocol possibly incuring a worser bad debt than this could have been.
Manuel review
Refactor the liquidation calculation, to allow liquidator to repay the debt and still get a reward out of this.
Context
#0 - c4-pre-sort
2024-04-28T10:09:20Z
JustDravee marked the issue as primary issue
#1 - c4-pre-sort
2024-04-29T09:24:25Z
JustDravee marked the issue as high quality report
#2 - shafu0x
2024-04-30T11:23:28Z
If the CR < 100, where should the reward come from?
#3 - koolexcrypto
2024-05-04T09:43:05Z
Looks like a systematic risk. If CR drops below 100 before it gets liquidated, there is a much bigger problem.
As the sponsor said, where the reward will come from?
#4 - c4-judge
2024-05-04T09:44:05Z
koolexcrypto changed the severity to QA (Quality Assurance)
#5 - c4-judge
2024-05-10T10:57:32Z
koolexcrypto marked the issue as grade-c
#6 - c4-judge
2024-05-12T09:23:56Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#7 - InfectedIsm
2024-05-15T21:34:11Z
Hi @koolexcrypto, @shafu0x
I don't think this is an unsolvable systemic issue, and this would be a mistake to not implement mechanism to take care underwater loans. This issue, and the remediations are well established in CDP protocols:
Thanks for taking a second look to the issue
Regards
#8 - shafu0x
2024-05-22T13:37:07Z
we will not implement something like an "insurance fund". do you have another mitigation option?
#9 - InfectedIsm
2024-05-22T14:50:02Z
@shafu0x I believe the other possibilities would be based on a debt socialization :
But I feel like a fund/balance, generated by fees taken from "good" liquidation, to cover for these rare situations is still the best way to go. Maybe fellow SRs have other ideas.
#10 - koolexcrypto
2024-05-28T16:11:35Z
Upgrading this to Medium due to the following:
1- The absence of a mechanism to allow liquidating bad debts (CR < 1) creates a potential risk for the protocol. 2- I couldn't find in the documentation/code that it is an accepted risk based on an intended design.
#11 - koolexcrypto
2024-05-28T16:19:14Z
award Kerosene from the 1B total supply as a bonus to cover missing part of collateral
This seems to be a good starting point. LP will be incentivised to liquidate, in return, they receive Kerosene. Kerosene then can be sold in the secondary market or re-used to mint DYAD.
However, consequences of this approach should be taken into account and addressed, for example, circulating Kerosene will increase which will influence the price.
CC: @shafu0x
#12 - c4-judge
2024-05-28T16:19:58Z
koolexcrypto removed the grade
#13 - c4-judge
2024-05-28T16:20:20Z
This previously downgraded issue has been upgraded by koolexcrypto
#14 - c4-judge
2024-05-28T16:20:46Z
koolexcrypto marked the issue as satisfactory
#15 - c4-judge
2024-05-28T16:20:57Z
koolexcrypto marked the issue as selected for report
#16 - GiorgioDalla
2024-05-29T10:34:38Z
since this is upgraded, could you consider dupe #288 ? thanks
π Selected for report: Circolors
Also found by: 0xtankr, AamirMK, Al-Qa-qa, FastChecker, Infect3d, SBSecurity, Strausses, T1MOH, VAD37, carrotsmuggler, gumgumzum
122.4433 USDC - $122.44
Judge has assessed an item in Issue #980 as 2 risk. The relevant finding follows:
L04 - Missing setUnboundedKerosenVault initialization in deploy https://github.com/code-423n4/2024-04-dyad/blob/49fb7174576e5147dc73d3222d16954aa641a2e0/script/deploy/Deploy.V2.s.sol#L89-L89
Vulnerability details boundedKerosineVault is deployed, but the setUnboundedKerosenVault is not called, which will cause a revert when BounderKerosineVault::assetPrice() will be called to price users kerosine collateral:
File: src/core/Vault.kerosine.bounded.sol 22: 23: function setUnboundedKerosineVault( 24: UnboundedKerosineVault _unboundedKerosineVault 25: ) 26: external 27: onlyOwner 28: { 29: unboundedKerosineVault = _unboundedKerosineVault; 30: } 31: ...: ...: /// ... some code ... ...: 44: function assetPrice() 45: public 46: view 47: override 48: returns (uint) { 49: return unboundedKerosineVault.assetPrice() * 2; 50: }
#0 - c4-judge
2024-05-29T08:29:49Z
koolexcrypto marked the issue as duplicate of #829
#1 - c4-judge
2024-05-29T11:23:20Z
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
https://github.com/code-423n4/2024-04-dyad/blob/49fb7174576e5147dc73d3222d16954aa641a2e0/src/core/Vault.kerosine.unbounded.sol#L65-L66 https://github.com/code-423n4/2024-04-dyad/blob/49fb7174576e5147dc73d3222d16954aa641a2e0/src/staking/KerosineDenominator.sol#L17-L22
The protocol sets Keroseneβs value as DYAD collateral deterministically (documentation):
The implementation can be found here: https://github.com/code-423n4/2024-04-dyad/blob/49fb7174576e5147dc73d3222d16954aa641a2e0/src/core/Vault.kerosine.unbounded.sol#L65-L66
File: src/core/Vault.kerosine.unbounded.sol 50: function assetPrice() 51: public 52: view 53: override 54: returns (uint) { 55: uint tvl; 56: address[] memory vaults = kerosineManager.getVaults(); 57: uint numberOfVaults = vaults.length; 58: for (uint i = 0; i < numberOfVaults; i++) { 59: Vault vault = Vault(vaults[i]); 60: tvl += vault.asset().balanceOf(address(vault)) 61: * vault.assetPrice() * 1e18 62: / (10**vault.asset().decimals()) 63: / (10**vault.oracle().decimals()); 64: } 65: uint numerator = tvl - dyad.totalSupply(); 66: uint denominator = kerosineDenominator.denominator(); 67: return numerator * 1e8 / denominator; 68: }
This price can easily be manipulated by an malicious actor (Alice) with sufficient funds:
tvl
will increase, dyad.totalSupply()
will stay the same, so numerator
will increase, thus Kerosene pricedenominator
will not changedyad.totalSupply()
, thus reducing numerator
and so Kerosine priceCR < MIN_COLLATERIZATION_RATIO
)dyad.totalSupply()
, causing a liquidation cascade and crashing Kerosine price.Please note that this effect is proportional to the supply controlled by the whales, but any amount used to operate that exploit will manipulate the price proportionally.
Unfair liquidation and Kerosine price crash
I have setup a test environment based on existing VaultManager.t.sol
and DeployBase.s.sol
, but had to make some additions to run the tests against VaultManagerV2
Thus will have to add these two files VaultManagerV2.t.sol
and DeloyBaseV2.sol
, available in the gist that follows: https://gist.github.com/InfectedIsm/111d71fb5265a1f62e3a248c64d5afa0
These files must be added at the same level as VaultManager.t.sol
and DeployBase.s.sol
respectively.
Output of the test:
Ran 1 test for test/VaultManagerV2.t.sol:VaultManagerV2Test [PASS] test_ManipulatePrice() (gas: 1046778) Logs: ker price: 150000 ker price: 100000 price change: -33%
Manual review
Do not use spot value to calculate Kerosine price, but rather an oracle (can be a TWAP based on historic balance/supply) The longer the TWAP window will be chosen, the smoother the price change will be, allowing users to anticipate the manipulation and act accordingly on their position.
MEV
#0 - c4-pre-sort
2024-04-28T05:19:32Z
JustDravee marked the issue as duplicate of #67
#1 - c4-pre-sort
2024-04-28T05:19:41Z
JustDravee marked the issue as high quality report
#2 - c4-judge
2024-05-05T09:59:11Z
koolexcrypto changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-05-08T11:50:02Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-08T12:06:38Z
koolexcrypto marked the issue as satisfactory