Platform: Code4rena
Start Date: 15/03/2024
Pot Size: $60,500 USDC
Total HM: 16
Participants: 43
Period: 21 days
Judge: hansfriese
Total Solo HM: 5
Id: 348
League: ETH
Rank: 27/43
Findings: 2
Award: $91.81
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Infect3d
Also found by: Evo, LinKenji, XDZIBECX, falconhoof, foxb868, ilchovski, klau5, nonseodion
67.2468 USDC - $67.25
https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/facets/BidOrdersFacet.sol#L130-L204 https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibOrders.sol#L556-L626 https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/facets/BidOrdersFacet.sol#L150 https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibOrders.sol#L574
bidMatchAlgo
and sellMatchAlgo
, which are responsible for matching incoming orders with existing orders in the orderbook based on price.
The bidMatchAlgo
and sellMatchAlgo
functions are expected to match incoming orders with existing orders in the orderbook based on accurate and up-to-date price information. The hint system should optimize the order placement process without compromising the integrity of the matching logic.
The expected inputs are the asset
being traded, the incomingBid
or incomingAsk
order, and the orderHintArray
for efficient order placement.
The intended outcome is to fill the incoming order as much as possible by matching it with existing orders at prices that reflect the current market conditions, ensuring fair and accurate trading for all users.
BidOrdersFacet.sol#bidMatchAlgo
function bidMatchAlgo( address asset, STypes.Order memory incomingBid, MTypes.OrderHint[] memory orderHintArray, MTypes.BidMatchAlgo memory b ) private returns (uint88 ethFilled, uint88 ercAmountLeft) { // ... code ... STypes.Order memory lowestSell = _getLowestSell(asset, b); if (incomingBid.price >= lowestSell.price) { // ... code ... LibOrders.updateOracleAndStartingShortViaThreshold(asset, LibOracle.getPrice(asset), incomingBid, shortHintArray); b.shortHintId = b.shortId = Asset.startingShortId; b.oraclePrice = LibOracle.getPrice(asset); return bidMatchAlgo(asset, incomingBid, orderHintArray, b); } // ... code ... }
function sellMatchAlgo( address asset, STypes.Order memory incomingAsk, MTypes.OrderHint[] memory orderHintArray, uint256 minAskEth ) internal { // ... code ... if (incomingAsk.price < oraclePrice) { LibOrders.addSellOrder(incomingAsk, asset, orderHintArray); } else { LibOrders.sellMatchAlgo(asset, incomingAsk, orderHintArray, minAskEth); } }
These functions use the cached oracle price and the provided order hints to match incoming orders with existing orders in the orderbook. The bidMatchAlgo
function updates the oracle price and startingShortId
if the price difference between the incoming bid and the cached oracle price exceeds a threshold. The sellMatchAlgo
function adds the incoming ask order to the orderbook if its price is below the cached oracle price.
The edge case arises from the combination of the 0.5% buffer, backwards matching logic, and the use of stale prices due to the 15-minute caching window.
The conditions that enable this
The codes responsible are:
bidMatchAlgo
, the comparison if (incomingBid.price >= lowestSell.price) allows matching based on the cached oracle price and the 0.5% buffer.sellMatchAlgo
, the condition if (incomingAsk.price <= highestBid.price) relies on the cached oracle price to determine whether to add the incoming ask order to the orderbook.The actual behavior of the system deviates from the expected behavior in the following ways:
These behaviors deviate from the intended fair and accurate matching of orders based on up-to-date price information.
Scenario:
Steps:
An attacker observes the market conditions and identifies an opportunity to exploit the vulnerability.
The attacker places a buy order for 1500 tokens at $99.75, which is within the 0.5% buffer of the cached oracle price ($100).
function placeBuyOrder(address asset, uint256 amount, uint256 price) external { STypes.Order memory order = STypes.Order({ addr: msg.sender, price: price, ercAmount: amount, id: nextOrderId, orderType: O.LimitBid, creationTime: block.timestamp }); MTypes.OrderHint[] memory orderHintArray; uint16[] memory shortHintArray; (uint88 ethFilled, uint88 ercAmountLeft) = bidMatchAlgo(asset, order, orderHintArray, shortHintArray); // Update user's balance and order state // ... }
The bidMatchAlgo
function is called with the attacker's buy order.
_getLowestSell
function returns the sell order with the price of $100.50 as the lowest sell order.if (incomingBid.price >= lowestSell.price)
is satisfied since the attacker's buy price of $99.75 is within the 0.5% buffer of the sell order price.updateOracleAndStartingShortViaThreshold
function is called, but since the price difference is within the threshold, no update occurs.The bidMatchAlgo
function proceeds to match the attacker's buy order with the existing sell orders.
The attacker successfully buys 1500 tokens at an average price of $100.67, which is significantly higher than the current market price of $95.
Potential Consequences:
Reduce the caching window:
function updateCachedPrice(address asset) internal { uint256 currentTime = block.timestamp; if (currentTime - lastPriceUpdateTime[asset] >= 5 minutes) { cachedPrice[asset] = fetchLatestPrice(asset); lastPriceUpdateTime[asset] = currentTime; } }
Implement price deviation checks:
function isValidPrice(address asset, uint256 price) internal view returns (bool) { uint256 cachedPrice = getCachedPrice(asset); uint256 realTimePrice = fetchRealTimePrice(asset); uint256 deviation = calculateDeviation(cachedPrice, realTimePrice); return deviation <= MAX_DEVIATION_THRESHOLD; }
Enhance the hint system:
function validateHint(address asset, uint256 hintPrice) internal view returns (bool) { uint256 cachedPrice = getCachedPrice(asset); uint256 realTimePrice = fetchRealTimePrice(asset); uint256 deviation = calculateDeviation(hintPrice, realTimePrice); return deviation <= MAX_HINT_DEVIATION_THRESHOLD; }
Implement circuit breakers:
function checkCircuitBreaker(address asset) internal view { uint256 priceDeviation = calculatePriceDeviation(asset); uint256 tradingVolume = calculateTradingVolume(asset); if (priceDeviation >= CIRCUIT_BREAKER_PRICE_THRESHOLD || tradingVolume >= CIRCUIT_BREAKER_VOLUME_THRESHOLD) { // Halt trading or limit order matching // ... } }
Context
#0 - c4-pre-sort
2024-04-07T04:43:36Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-04-07T04:43:45Z
raymondfam marked the issue as duplicate of #114
#2 - raymondfam
2024-04-07T04:45:20Z
Kind of unstructured in terms of POC when compared to that of #114 and #128.
#3 - c4-judge
2024-04-11T16:12:45Z
hansfriese marked the issue as satisfactory
🌟 Selected for report: popeye
Also found by: 0xbrett8571, JcFichtner, LinKenji, Rhaydden, SAQ, Sathish9098, albahaca, clara, emerald7017, fouzantanveer, foxb868, hunter_w3b, kaveyjoe, roguereggiant
24.5635 USDC - $24.56
DittoETH is a decentralized stablecoin protocol that aims to provide stable assets backed by over-collateralized staked ETH. It utilizes an order book design to match buyers and sellers, allowing for efficient price discovery and liquidity provision. The protocol incentivizes liquidity providers with supercharged staking yields, making it attractive for users to participate in the system.
1. Code Review:
2. Architecture Review:
3. Mechanism Review:
4. Centralization and Admin Control:
5. Systemic Risks:
Based on the Codebase, here are some of my observations regarding the codebase quality:
1. Modularization and Separation of Concerns:
ExitShortFacet
).2. Use of Libraries and Interfaces:
LibSRUtil
, LibOracle
) and interfaces (e.g., IUniswapV3Pool
), which promotes code reusability and abstraction.3. Error Handling and Validation:
isNotFrozen
, onlyValidShortRecord
).1. Complexity and Interdependencies:
2. Oracle Dependency:
3. Liquidation and Margin Call Mechanisms:
4. Upgradability and Contract Migration:
1. Admin Roles and Privileges:
onlyDAO
, which grant special privileges to certain addresses.2. Governance Centralization:
1. Order Matching:
bidMatchAlgo
and sellMatchAlgo
functions handle the matching of incoming orders with existing orders in the orderbook.2. Short Order Matching:
bidMatchAlgo
function updates the oracle price and the startingShortId
based on certain thresholds.3. Liquidation and Margin Calls:
liquidate
function in the PrimaryLiquidationFacet
contract allows for the liquidation of undercollateralized short positions.1. Dependency on External Protocols:
2. Liquidity and Market Dynamics:
3. Stablecoin Peg Stability:
bidMatchAlgo
function, the updateOracleAndStartingShortViaThreshold
function is called to update the oracle price based on certain thresholds.function bidMatchAlgo( address asset, STypes.Order memory incomingBid, MTypes.OrderHint[] memory orderHintArray, MTypes.BidMatchAlgo memory b ) private returns (uint88 ethFilled, uint88 ercAmountLeft) { // ... STypes.Order memory lowestSell = _getLowestSell(asset, b); if (incomingBid.price >= lowestSell.price) { // ... LibOrders.updateOracleAndStartingShortViaThreshold(asset, LibOracle.getPrice(asset), incomingBid, shortHintArray); // ... } // ... }
2. Front-Running Vulnerability:
bidMatchAlgo
and sellMatchAlgo
functions match incoming orders with existing orders based on price.function bidMatchAlgo( address asset, STypes.Order memory incomingBid, MTypes.OrderHint[] memory orderHintArray, MTypes.BidMatchAlgo memory b ) private returns (uint88 ethFilled, uint88 ercAmountLeft) { // ... while (true) { // ... STypes.Order memory lowestSell = _getLowestSell(asset, b); if (incomingBid.price >= lowestSell.price) { matchlowestSell(asset, lowestSell, incomingBid, matchTotal); // ... } // ... } } function sellMatchAlgo( address asset, STypes.Order memory incomingAsk, MTypes.OrderHint[] memory orderHintArray, uint256 minAskEth ) internal { // ... while (true) { STypes.Order memory highestBid = s.bids[asset][startingId]; if (incomingAsk.price <= highestBid.price) { matchHighestBid(incomingAsk, highestBid, asset, matchTotal); // ... } // ... } }
3. Liquidation Incentives and Risks:
liquidate
function in the PrimaryLiquidationFacet
contract allows for the liquidation of undercollateralized short positions.callerFee
and gasFee
) are adequate to attract liquidators and consider implementing safeguards against malicious liquidations, such as price thresholds and time delays.function liquidate(address asset, address shorter, uint8 id, uint16[] memory shortHintArray, uint16 shortOrderId) external isNotFrozen(asset) nonReentrant onlyValidShortRecord(asset, shorter, id) returns (uint88, uint88) { // ... _performForcedBid(m, shortHintArray); _liquidationFeeHandler(m); // ... } function _liquidationFeeHandler(MTypes.PrimaryLiquidation memory m) private { // ... uint88 callerFee = m.ethFilled.mulU88(m.callerFeePct) + m.gasFee; // ... }
Recommendations:
Implement robust oracle solutions: Ensure the reliability and security of the price oracle mechanisms used in the protocol. Consider using multiple oracle sources, implementing price deviation checks, and establishing fallback mechanisms to handle oracle failures or manipulations.
Enhance front-running protection: Implement measures to prevent front-running attacks, such as order batching, random order matching, or the use of a commit-reveal scheme. Consider utilizing technologies like zero-knowledge proofs or secure multi-party computation to enhance transaction privacy.
Strengthen governance and admin controls: Clearly define and document the roles and responsibilities of admin and governance functions. Implement multi-signature requirements, time-locks, and other safeguards to prevent unauthorized or malicious actions by privileged accounts.
Develop comprehensive test suites, including unit tests, integration tests, and scenario-based tests, to ensure the correctness and robustness of the protocol's mechanisms. Perform regular audits and security assessments to identify and address potential vulnerabilities.
Enhance the codebase documentation, including detailed comments, function descriptions, and high-level overviews. Ensure that the code is well-structured, modular, and follows best practices for readability and maintainability.
Develop and document contingency plans to handle potential risks, such as market volatility, liquidity crises, or system failures. Define clear procedures for emergency interventions, including the use of circuit breakers and emergency governance actions.
1. Oracle Manipulation:
function bidMatchAlgo( address asset, STypes.Order memory incomingBid, MTypes.OrderHint[] memory orderHintArray, MTypes.BidMatchAlgo memory b ) private returns (uint88 ethFilled, uint88 ercAmountLeft) { // ... LibOrders.updateOracleAndStartingShortViaThreshold(asset, LibOracle.getPrice(asset), incomingBid, shortHintArray); // ... }
2. Front-Running Vulnerability:
function bidMatchAlgo( address asset, STypes.Order memory incomingBid, MTypes.OrderHint[] memory orderHintArray, MTypes.BidMatchAlgo memory b ) private returns (uint88 ethFilled, uint88 ercAmountLeft) { // ... while (true) { // ... STypes.Order memory lowestSell = _getLowestSell(asset, b); if (incomingBid.price >= lowestSell.price) { matchlowestSell(asset, lowestSell, incomingBid, matchTotal); // ... } // ... } }
function sellMatchAlgo( address asset, STypes.Order memory incomingAsk, MTypes.OrderHint[] memory orderHintArray, uint256 minAskEth ) internal { // ... while (true) { STypes.Order memory highestBid = s.bids[asset][startingId]; if (incomingAsk.price <= highestBid.price) { matchHighestBid(incomingAsk, highestBid, asset, matchTotal); // ... } // ... } }
Susceptibility of the order matching process to sandwich attacks, where an attacker can manipulate the price by placing orders both before and after a pending order, potentially leading to financial losses for the sandwiched user.
bidMatchAlgo
and sellMatchAlgo
, are responsible for matching incoming orders with existing orders in the orderbook based on price.
function bidMatchAlgo( address asset, STypes.Order memory incomingBid, MTypes.OrderHint[] memory orderHintArray, MTypes.BidMatchAlgo memory b ) private returns (uint88 ethFilled, uint88 ercAmountLeft) { // ... code ... while (true) { // ... code ... STypes.Order memory lowestSell = _getLowestSell(asset, b); if (incomingBid.price >= lowestSell.price) { matchlowestSell(asset, lowestSell, incomingBid, matchTotal); // ... code ... } else { // ... code ... return matchIncomingBid(asset, incomingBid, matchTotal, b); } } }
function sellMatchAlgo( address asset, STypes.Order memory incomingAsk, MTypes.OrderHint[] memory orderHintArray, uint256 minAskEth ) internal { // ... code ... while (true) { STypes.Order memory highestBid = s.bids[asset][startingId]; if (incomingAsk.price <= highestBid.price) { matchHighestBid(incomingAsk, highestBid, asset, matchTotal); // ... code ... } else { // ... code ... return; } } }
These functions iterate through the orderbook, matching the incoming order with the lowest sell order or highest bid order, respectively, based on price.
The lack of protection against sandwich attacks in the order matching process. An attacker can exploit this vulnerability by placing orders both before and after a pending order, manipulating the price in their favor.
The specific conditions that enable this vulnerability are:
Lines of code responsible for the vulnerability.
bidMatchAlgo
, the comparison if (incomingBid.price >= lowestSell.price)
matches the incoming bid with the lowest sell order based on price.sellMatchAlgo
, the comparison if (incomingAsk.price <= highestBid.price)
matches the incoming ask with the highest bid order based on price.Recommendation
Introduce a random order matching mechanism:
function matchOrders(address asset, STypes.Order memory incomingOrder) internal { // ... code ... uint256 randomValue = getRandomValue(); uint256 orderIndex = randomValue % orders.length; STypes.Order memory matchingOrder = orders[orderIndex]; // ... code ... }
Implement a time-based order priority:
function addOrder(STypes.Order memory order) internal { order.timestamp = block.timestamp; // ... code ... } function matchOrders(address asset, STypes.Order memory incomingOrder) internal { // ... code ... STypes.Order[] memory sortedOrders = sortOrdersByTimestamp(orders); // ... code ... }
Use a batch auction mechanism:
function batchAuction(address asset) external { // ... code ... STypes.Order[] memory batchedOrders = collectOrders(asset); // ... code ... executeBatchOrders(batchedOrders); // ... code ... }
Implement monitoring and detection mechanisms:
function monitorTrading(address asset) external { // ... code ... STypes.Order[] memory recentOrders = getRecentOrders(asset); bool isSuspicious = detectSuspiciousActivity(recentOrders); if (isSuspicious) { // Flag the suspicious activity and take appropriate actions // ... } // ... code ... }
4. Liquidity Provider Risks:
5. Governance Centralization Risks:
7. Upgradability Risks:
8. Flash Loan Risks:
9. Code Quality and Security Best Practices:
These are some of the weaknesses i may have noticed from the CodeBase, centralization risks, and improvement points identified in the DittoETH codebase.
The DittoETH system consists of several key components:
Vaults: Vaults hold the collateral (staked ETH) and are used to mint stable assets. Each vault can support multiple stable assets.
Bridges: Bridges allow users to deposit and withdraw their staked ETH (such as rETH and stETH) into and out of the vaults.
Orderbook: The orderbook is a decentralized exchange mechanism that matches buy and sell orders for stable assets. It supports limit orders, market orders, and short orders.
ShortRecord: ShortRecords represent the debt positions of users who have minted stable assets by providing collateral. They track the collateral ratio and allow for liquidation if the ratio falls below a certain threshold.
Liquidation Mechanism: The liquidation mechanism ensures that the system remains solvent by liquidating undercollateralized ShortRecords and redistributing the collateral to maintain the stability of the stable assets.
Oracle: The oracle provides price feeds for the assets in the system, enabling accurate pricing and collateralization calculations.
The DittoETH various functions that enable the core functionalities of the system:
1. Vault Functions:
createVault
: Creates a new vault to hold collateral and mint stable assets.depositAsset
: Allows users to deposit stable assets into the vault.withdrawAsset
: Allows users to withdraw stable assets from the vault.2. Bridge Functions:
deposit
: Allows users to deposit staked ETH (e.g., rETH, stETH) into the vault.depositEth
: Allows users to deposit ETH directly into the vault, which is then converted to staked ETH.withdraw
: Allows users to withdraw their staked ETH from the vault.3. Orderbook Functions:
createBid
: Creates a buy order in the orderbook.createAsk
: Creates a sell order in the orderbook.createLimitShort
: Creates a limit short order in the orderbook.cancelOrder
: Cancels an existing order in the orderbook.4. ShortRecord Functions:
createShortRecord
: Creates a new ShortRecord when a short order is matched.updateShortRecord
: Updates the collateral and debt of a ShortRecord.liquidateShortRecord
: Liquidates an undercollateralized ShortRecord.5. Liquidation Functions:
liquidate
: Initiates the liquidation process for an undercollateralized ShortRecord.redistributeCollateral
: Redistributes the collateral of a liquidated ShortRecord to maintain system solvency.6. Oracle Functions:
getPrice
: Retrieves the current price of an asset from the oracle.setPrice
: Sets the price of an asset in the oracle (admin function).Users: Regular users who interact with the DittoETH protocol to mint stable assets, provide liquidity, or trade on the orderbook.
Liquidity Providers (LPs): Users who deposit their staked ETH into the vaults to provide liquidity and earn staking yields.
Shorters: Users who mint stable assets by creating short positions and providing collateral.
Liquidators: Users or bots that monitor the system for undercollateralized ShortRecords and initiate the liquidation process to maintain system solvency.
Oracles: External entities that provide price feeds for the assets in the system.
Admin: An administrative role with privileged access to certain functions, such as setting oracle prices or managing system parameters.
The DittoETH protocol follows a decentralized architecture, with smart contracts deployed on the Ethereum blockchain. The high-level workflow of the system is as follows:
Users deposit their staked ETH (rETH, stETH) into the vaults through the bridge contracts.
Shorters create short positions by minting stable assets and providing collateral. They specify the collateral ratio and the amount of stable assets they wish to mint.
Buyers and sellers place orders on the orderbook, specifying the price and quantity of stable assets they want to trade.
The orderbook matches buy and sell orders based on price priority. When a match occurs, the stable assets are exchanged, and the collateral is updated accordingly.
If a shorter's collateral ratio falls below the liquidation threshold, liquidators can initiate the liquidation process to close the short position and redistribute the collateral.
The oracle provides real-time price feeds for the assets in the system, ensuring accurate pricing and collateralization calculations.
Users can withdraw their staked ETH from the vaults through the bridge contracts, subject to any applicable fees or lock-up periods.
The DittoETH protocol utilizes various mathematical models and algorithms to ensure the stability of the minted assets, calculate collateral ratios, and determine liquidation thresholds. The system is designed to be self-sustaining and resistant to market volatility and manipulation.
Overall, DittoETH aims to provide a decentralized and efficient platform for minting stable assets backed by staked ETH, while offering attractive staking yields to liquidity providers. The order book design enables price discovery and liquidity, while the liquidation mechanism ensures the solvency and stability of the system.
30 hours
#0 - c4-pre-sort
2024-04-07T20:25:14Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2024-04-17T07:11:10Z
hansfriese marked the issue as grade-b