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: 105/183
Findings: 2
Award: $8.69
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/Vault.kerosine.unbounded.sol#L50-#L68
The current deployment setup enables permanent DoS attack on the VaultManagerV2
contract. The assetPrice()
function in the UnboundedKerosineVault.sol
contract will keep reverting until the tvl
surpasses the total supply of DYAD tokens.
This makes the VaultManagerV2
contract unusable.
By observing the assetPrice()
function in the Vault.kerosine.unbounded.sol
file, we can see that the numerator is the difference between tvl and dyad total supply.
tvl
is calculated as the sum of all the assets in the vaults that were whitleisted in the KerosineManager
contract.
## UnboundedKerosineVault.sol function assetPrice() public view override returns (uint256) { uint256 tvl; >>> address[] memory vaults = kerosineManager.getVaults(); uint256 numberOfVaults = vaults.length; for (uint256 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()); } >>> uint256 numerator = tvl - dyad.totalSupply(); uint256 denominator = kerosineDenominator.denominator(); return numerator * 1e8 / denominator; }
On the other had the deployment script DeployV2
deploys new weth
and wsteth
vaults to be used with the new VaultManagerV2
contract. It uses already existing DYAD contract on mainnet.
At the time of writing this report, the totalSupply
of DYAD is: 632967400000000000000000 or 632967.4 DYAD tokens.
As both vaults are deployed with 0 balance, the assetPrice()
function will revert because the tvl
is less than the dyad.totalSupply()
. It will keep reverting until the tvl
is greater than the dyad.totalSupply()
.
As assetPrice()
from UnboundedKerosineVault.sol
is used in the VaultManagerV2
contract, as part of getKeroseneValue()
function, almost all the functions in the VaultManagerV2
contract will revert.
This makes mintDyad
function unusable.
There is no clear migration path from the old VaultManager
to the new VaultManagerV2
contract with the current setup.
The existing vaults, i.e. WETH_VAULT
and WSTETH_VAULT
have the following logic:
contract Vault is IVault { using SafeTransferLib for ERC20; using SafeCast for int256; using FixedPointMathLib for uint256; uint256 public constant STALE_DATA_TIMEOUT = 90 minutes; IVaultManager public immutable vaultManager; ERC20 public immutable asset; IAggregatorV3 public immutable oracle; mapping(uint256 => uint256) public id2asset; ... function withdraw(uint256 id, address to, uint256 amount) external onlyVaultManager { >>> id2asset[id] -= amount; asset.safeTransfer(to, amount); emit Withdraw(id, to, amount); } ... } contract VaultManager is IVaultManager { ... function withdraw(uint256 id, address vault, uint256 amount, address to) public isDNftOwner(id) { >>> Vault(vault).withdraw(id, to, amount); if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); } ... }
vaultManager
is immutable in both vaults. Withdrawal can only be done by the currently deployed VaultManager
contract.Also, the VaultManager
can't simply be unlicensed from the DYAD
contract because then users can't burn their DYAD tokens and withdraw the assets from the vaults.
To get back to the initial claim, until the tvl
in the new vaults surpasses the total supply of DYAD, the new VaultManagerV2
contract will keep reverting.
This is a permanent DoS attack on the VaultManagerV2
contract as the old VaultManager
can't be unlicensed from the DYAD contract and malicious griever can keep on minting DYAD through the old VaultManager
contract.
As a consequence, the new VaultManagerV2
contract will keep reverting.
Devise a migration plan from the old VaultManager
to the new VaultManagerV2
contract that takes into account the existing vaults and the DYAD token holders.
DoS
#0 - c4-pre-sort
2024-04-27T18:17:12Z
JustDravee marked the issue as primary issue
#1 - c4-pre-sort
2024-04-27T18:17:15Z
JustDravee marked the issue as high quality report
#2 - shafu0x
2024-04-30T11:29:04Z
Good find! It should only count the dyad minted through V2.
#3 - c4-judge
2024-05-05T13:48:50Z
koolexcrypto marked the issue as duplicate of #308
#4 - c4-judge
2024-05-11T20:09:45Z
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/main/src/core/Vault.kerosine.unbounded.sol#L65
Kerosene price depends on total value locked inside the regular vaults which makes it subject to manipulation. This can be used in a bat and switch attack to liquidate users. It can also be used to mint DYAD tokens with much lower actual collateralization ratio than 150%. In turn, this can leave the protocol in undercollateralized state.
The new VaultManagerV2
contract has introduced the Kerosene vaults. Users has the following possibility:
/// @inheritdoc IVaultManager function mintDyad(uint256 id, uint256 amount, address to) external isDNftOwner(id) { uint256 newDyadMinted = dyad.mintedDyad(address(this), id) + amount; >>> if (getNonKeroseneValue(id) < newDyadMinted) revert NotEnoughExoCollat(); // So minted dyad is capped by the value of your collateral. How is that over collateralized? dyad.mint(id, to, amount); >>> if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); emit MintDyad(id, amount, to); }
An example of how this work is as follows:
If the price of ETH drops or the price of Kerosene drops, he will be subject to liquidation.
Price of ETH can't be manipulated, so it is reasonable to he gets liquidated if it drops, and he is expected to monitor his position and provide additional collateral if needed.
However, the price of Kerosene is subject to manipulation. A malicious actor can set up a bait and switch attack to liquidate the users.
Another issue with this manipulation property of Kerosene is that users are able to mint DYAD tokens with 1:1 colateralization ratio for ETH and WSTETH. While the additional 50% can be provided by Kerosene. Given the inflated price of Kerosene detailed in the description above, attacker can mint DYAD with much lower actual collateralization ratio than 150%. This is a serious issue because it means that if the price of actual assets(ETH and WSTETH) drops, it leaves the protocol in undercollateralized state.
One of the possibilities for tackling the issue is requiring non-Kerosene collateral to be above 100%. It can have a value of 120% for example while the rest can be provided with Kerosene.
Other
#0 - c4-pre-sort
2024-04-28T05:08:49Z
JustDravee marked the issue as duplicate of #67
#1 - c4-pre-sort
2024-04-28T05:15:16Z
JustDravee marked the issue as high quality report
#2 - c4-judge
2024-05-08T11:50:02Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-08T12:06:28Z
koolexcrypto marked the issue as satisfactory