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: 109/183
Findings: 3
Award: $7.82
🌟 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
DYAD
is per description a stablecoin which has a collateralization ratio of 150% meaning for every 1.5$
worth of exogenous collateral we deposit into a vault we should only ever be able to mint 1 DYAD
. Unfortunatly there exists a critical issue in the VaultManagerV2
contract making it possible to mint 1 DYAD for 1$
worth of collateral.
Looking at VaultManagerV2
, a user can add vaults
which contain exogenous collateral and vaultsKerosene
which contain Kerosene. For both these mappings, there is one function (add
and addKerosene
) to add vaults to a user's dNFT
.
function add( uint id, address vault ) external isDNftOwner(id) { if (vaults[id].length() >= MAX_VAULTS) revert TooManyVaults(); if (!vaultLicenser.isLicensed(vault)) revert VaultNotLicensed(); 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(); if (!vaultsKerosene[id].add(vault)) revert VaultAlreadyAdded(); emit Added(id, vault); }
In order to add a vault to these mappings, the vault needs to be licensed which can only be done by the vaultLicenser
and the keroseneManager
respectively. Looking at the deploy script Deploy.V2.sol
we can see that for the exogenous vaults ethVault
and wstEth
get licensed. For the kerosene vaults, ethVault
, wstEth
and unboundedKerosineVault
get licensed. The issue arises because ethVault
and wstEth
get licensed for both exogenous vaults
and kerosene vaultsKerosene
. This means a user can add the ethVault
to their vaults
and vaultsKerosene
.
Let's have a look at the 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(); // [1] dyad.mint(id, to, amount); if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); // [2] (MIN_COLLATERALIZATION_RATIO = 1.5e18 = 150%) emit MintDyad(id, amount, to); }
At [1]
we check if we deposited enough exogenous collateral to theoretically mint the wanted amount of DYAD
.
Then at [2]
after minting the DYAD
we check if we adhere to the 150% collateralization rate.
Looking at collatRatio
we can see:
function collatRatio( uint id ) public view returns (uint) { uint _dyad = dyad.mintedDyad(address(this), id); if (_dyad == 0) return type(uint).max; return getTotalUsdValue(id).divWadDown(_dyad); } function getTotalUsdValue( uint id ) public view returns (uint) { return getNonKeroseneValue(id) + getKeroseneValue(id); }
Here we take the amount of DYAD
we ever minted and divide the totalUsdValue
of our dNFT
by that number.
In getTotalUsdValue
we just add the nonKeroseneValue
and the keroseneValue
of our dNFT
.
These functions iterate over all the objects in vaults
and vaultsKerosene
respectively, returning the sum of the values of the vaults.
The value of a vault is taken by calling vault.getUsdValue
. If we now added the same vault to vaults
and vaultsKerosene
, getTotalUsdValue
will return a value twice as high as the actual value we deposited into that vault.
If we now assume we deposited 0.1 ether
and assume this is worth 300$
, we should only be able to mint 300/1.5 = 200
DYAD
. But since we inflated the totalUsdValue
of our dNFT
we can now mint 600/1.5 = 400
DYAD
without triggering the check at [2]
but cannot mint more than 300 DYAD
due to the check at [1]
.
This causes the DYAD
to not be 150% collateralized anymore causing the stablecoin to be possibly less stable and more volatile.
add the following contract to test/NFTHolder.sol
pragma solidity =0.8.17; import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; contract NFTHolder is ERC721Holder { constructor() {} fallback() external payable {} }
add the following function to test/fork/v2test.sol
function testAddNonKeroseneVaultToKeroseneVaults() public { NFTHolder holder = new NFTHolder(); vm.deal(address(holder), 1 ether); uint nft_id1 = DNft(MAINNET_DNFT).mintNft{value: 1 ether}(address(holder)); uint nft_id2 = DNft(MAINNET_DNFT).mintNft{value: 1 ether}(address(holder)); // get 1 WETH vm.prank(address(MAINNET_WETH)); ERC20(MAINNET_WETH).transferFrom(address(MAINNET_WETH), address(holder), 1 ether); vm.startPrank(address(holder)); console.log("Holder's DYAD before: ", Dyad(MAINNET_DYAD).balanceOf(address(holder))); console.log("Holder's WETH before: ", ERC20(MAINNET_WETH).balanceOf(address(holder))); ERC20(MAINNET_WETH).approve(address(contracts.vaultManager), 10 ether); contracts.vaultManager.deposit(nft_id1, address(contracts.ethVault), 0.1 ether); // first issue: we can add the ethVault to both "vaults" and "vaultsKerosene", this causes all the following issues contracts.vaultManager.add(nft_id1, address(contracts.ethVault)); contracts.vaultManager.addKerosene(nft_id1, address(contracts.ethVault)); Vault vault = Vault(contracts.ethVault); uint etherValue = 0.1 ether * vault.assetPrice() * 1e18 / 10**vault.oracle().decimals() / 10**vault.asset().decimals(); // second issue: we can mint 100% of our deposited value due to the CR check not triggering because we added the same vault twice // making the contract think we added twice the value than we actually did contracts.vaultManager.mintDyad(nft_id1, etherValue, address(holder)); console.log("Holder's DYAD after: ", Dyad(MAINNET_DYAD).balanceOf(address(holder))); console.log("Holder's WETH after: ", ERC20(MAINNET_WETH).balanceOf(address(holder))); vm.stopPrank(); }
Manual review
Either do not license vaults for both keroseneManager
and vaultLicenser
, or add a check in the add*
functions to check whether the vault's asset is Kerosene or any exogenous asset.
Other
#0 - c4-pre-sort
2024-04-29T05:25:59Z
JustDravee marked the issue as duplicate of #966
#1 - c4-pre-sort
2024-04-29T08:37:44Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-04T09:46:26Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-29T11:19:44Z
koolexcrypto marked the issue as duplicate of #1133
#4 - c4-judge
2024-05-29T14:03:23Z
koolexcrypto marked the issue as satisfactory
🌟 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
As stated in the description, DYAD
should always be backed by 150% exogenous collateral. This should be achieved by having separate vaults for on one side Kerosene
and on the other side WETH
and WSTETH
.
These two mappings vaults
and vaultsKerosene
are taken respectively when calculating either the exogenous or Kerosene value of a user's dNFT
.
Due to this, vaults
should only ever contain vaults containing exogenous collateral. This is furthermore enforced by having two separate licensers for both mappings.
Unfortunately, in the deploy script a Kerosene vault (unboundedKerosineVault
) gets licensed for the exogenous collateral mapping.
This causes a user to be able to add the unboundedKerosineVault
to their vaults
mapping causing them to be able to mint DYAD
against Kerosene.
This would cause the stablecoin to become unstable and allow positions to be backed fully only by Kerosene, negatively impacting the protocol and violating one of its basic concepts.
add the following contract to test/NFTHolder.sol
pragma solidity =0.8.17; import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; contract NFTHolder is ERC721Holder { constructor() {} fallback() external payable {} }
add the following function to test/fork/v2test.sol
function testAbleToAddKeroseneVaultToVaults() public { NFTHolder holder = new NFTHolder(); vm.deal(address(holder), 1 ether); uint nft_id1 = DNft(MAINNET_DNFT).mintNft{value: 1 ether}(address(holder)); vm.startPrank(address(holder)); // this should not be possible but it is contracts.vaultManager.add(nft_id1, address(contracts.unboundedKerosineVault)); vm.stopPrank(); }
Executing this test will succeed, adding the unboundedKerosineVault
to the exogenous vaults
.
Manual review
Do not license Kerosene vaults for the vaultLicenser
Other
#0 - c4-pre-sort
2024-04-29T05:42:48Z
JustDravee marked the issue as duplicate of #966
#1 - c4-pre-sort
2024-04-29T08:37:44Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-04T09:46:26Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-29T11:19:45Z
koolexcrypto marked the issue as duplicate of #1133
#4 - c4-judge
2024-05-29T14:03:26Z
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
When calculating the assetPrice
in KerosineManager
the vaults' values are added to get the TVL. Later the dyad.totalSupply()
gets subtracted from this. If the TVL is lower than the DYAD total supply, this action reverts with an underflow error. In the contest's description it is stated that there is an invariant TVL > DYAD total supply
. This invariant however is not always true. When the protocol gets updated to V2, new vaults ethVault
and wstEth
are created. At the time of deployment these vaults contain no assets meaning the TVL
at the time of transition is 0 because no funds get transfered into these vaults in the deploy script. Therefore the TVL
will be lower than the total DYAD
supply until enough ether has been deposited into these vaults or has been transferred from the old V1 vaults.
The Kerosine price cannot be calculated because the function assetPrice()
will revert with an arithmetic underflow or overflow
error until the TVL is at least equal to the total DYAD total supply. This also causes the Kerosene vaults to be unusable until then. This is especially impactful for users since the unboundedKerosineVault
is licensed in the deploy script but essentially unusable.
add the following to test/fork/v2test.sol
// The following reverts in assetPrice() function testCannotRetrieveKerosenePrice() public { console.log(contracts.unboundedKerosineVault.assetPrice()); }
When executing this test, it will revert and we won't be able to retrieve the current Kerosene price.
Manual review
Transfer assets from the old vaults into the new ones.
Under/Overflow
#0 - c4-pre-sort
2024-04-27T18:23:23Z
JustDravee marked the issue as duplicate of #958
#1 - c4-pre-sort
2024-04-29T08:39:15Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-05T13:48:37Z
koolexcrypto marked the issue as duplicate of #308
#3 - c4-judge
2024-05-11T20:09:21Z
koolexcrypto marked the issue as satisfactory
#4 - c4-judge
2024-05-13T18:34:04Z
koolexcrypto changed the severity to 3 (High 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
In the deploy script, the only vaults that get licensed in the kerosineManager
are ethVault
and wstEth
but no Kerosene vault.
This causes users to be unable to use kerosene vaults as intended.
add the following contract to test/NFTHolder.sol
pragma solidity =0.8.17; import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; contract NFTHolder is ERC721Holder { constructor() {} fallback() external payable {} }
add the following function to test/fork/v2test.sol
function testCannotAddKeroseneVault() public { NFTHolder holder = new NFTHolder(); vm.deal(address(holder), 1 ether); uint nft_id1 = DNft(MAINNET_DNFT).mintNft{value: 1 ether}(address(holder)); vm.startPrank(address(holder)); // this reverts even though it should not contracts.vaultManager.addKerosene(nft_id1, address(contracts.unboundedKerosineVault)); vm.stopPrank(); }
This will revert with VaultNotLicensed()
even though it needs to be licensed for proper use of the protocol.
Manual review
License the unboundedKerosineVault
for the kerosineManager
in the deploy script.
Other
#0 - c4-pre-sort
2024-04-27T17:15:55Z
JustDravee marked the issue as duplicate of #70
#1 - c4-pre-sort
2024-04-29T12:02:22Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:58:57Z
koolexcrypto marked the issue as satisfactory