A decentralized stablecoin protocol with an order book design for supercharged staking yield.
Platform: Code4rena
Start Date: 15/03/2024
End Date: 05/04/2024
Period: 21 days
Status: Completed
Pot Size: $60,500 USDC
Participants: 43
Reporter: thebrittfactor
Judge: hansfriese
Id: 348
League: ETH
klau5 | 1/43 | $7,224.33 | 7 | 2 | 0 | 4 | 2 | - | 0 | 0 |
nonseodion | 2/43 | $6,433.02 | 6 | 2 | 0 | 3 | 0 | - | 0 | 0 |
00xSEV | 3/43 | $6,092.51 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
d3e4 | 4/43 | $6,092.51 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
Cosine | 5/43 | $5,209.10 | 3 | 2 | 0 | 1 | 0 | 0 | 0 | 0 |
serial-coder | 6/43 | $4,573.16 | 5 | 2 | 0 | 2 | 0 | - | 0 | 0 |
0xbepresent | 7/43 | $3,392.29 | 4 | 1 | 0 | 2 | 0 | - | 0 | 0 |
samuraii77 | 8/43 | $2,741.63 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
Infect3d | 9/43 | $1,975.19 | 4 | 0 | 0 | 3 | 1 | - | 0 | 0 |
ilchovski | 10/43 | $1,332.62 | 2 | 1 | 0 | 1 | 0 | 0 | 0 | 0 |
Auditor per page
The 4naly3er report can be found here.
Automated findings output for the audit can be found here within 24 hours of audit opening.
Note for C4 wardens: Anything included in this Automated Findings / Publicly Known Issues
section is considered a publicly known issue and is ineligible for awards.
dittoShorterRate
can give more/less ditto than expected (see L-21).minShortErc
requirements because of ercDebtRate
applicationdisburseCollateral
in proposeRedemption()
can cause user to lose yield if their SR was recently modified and it’s still below 2.0 CR (modified through order fill, or increase collateral)recoveryCR
in secondary liquidation unlike primary, may introduce later.minShortErc
) to prevent scenarios of ercDebt under minShortErc
claimRemainingCollateral()
is called on a SR that is included in a proposal and is later correctly disputed.The Ditto protocol is a new decentralized stable asset protocol for Ethereum mainnet. It takes in overcollateralized liquid staked ETH (rETH, stETH) to create stablecoins using a gas optimized orderbook (starting with a USD stablecoin, dUSD).
On the orderbook, bidders and shorters bring ETH, askers can sell their dUSD. Bidders get the dUSD, shorters get the bidders collateral and a ShortRecord to manage their debt position (similar to a CDP). Shorters get the collateral of the position (and thus the LST yield), with the bidder getting the stable asset, rather than a CDP where the user also gets the asset.
I'm happy to answer any questions on the discord/twitter!
See scope.txt
Contract | nSLOC | Purpose | Changes | External Libraries |
---|---|---|---|---|
facets/BidOrdersFacet.sol | 234 | Facet for creating and matching bids | dust | |
facets/ShortOrdersFacet.sol | 54 | Facet for creating and matching short orders | SR created at Order, recoveryMode, minShortErc | |
facets/PrimaryLiquidationFacet.sol | 173 | Facet for liquidation using ob | minShortErc, remove flagging, recovery | |
facets/BridgeRouterFacet.sol | 101 | Facet to handle depositing and withdrawing LSTs | Credit mechanism for withdraw | IBridge |
facets/ExitShortFacet.sol | 126 | Facet for a shorter to exit their short | minShortErc | IDiamond.createForcedBid |
facets/RedemptionFacet.sol | 241 | Ability to swap dUSD for ETH, akin to Liquity | new | |
libraries/LibBridgeRouter.sol | 151 | Helper library used in BridgeRouterFacet | new | Uniswap |
libraries/LibBytes.sol | 35 | Library in RedemptionFacet to save proposals in SSTORE2 | new | |
libraries/LibOracle.sol | 125 | Library to get price with Chainlink + backup | handle revert | Chainlink/Uniswap |
libraries/LibOrders.sol | 575 | Library Order Facets to do matching | dust, oracle price changes, auto adjust dethTithePercent | |
libraries/LibSRUtil.sol | 112 | Library of misc SR fns: recovery CR, minShortErc, transfer | new | |
libraries/UniswapOracleLibrary.sol | 34 | Used to get TWAP from Uniswap | didn't audit earlier | Uniswap |
Out of scope but helpful or necessary to understand the system (notes/changes from last audit in parens)
contracts/bridges/BridgeReth.sol
(addition of getUnitDethValue()
)contracts/bridges/BridgeSteth.sol
(addition of getUnitDethValue()
)facets/AskOrdersFacet.sol
facets/ERC721Facet.sol
(new checks to mint)facets/MarketShutdownFacet.sol
facets/OrdersFacet.sol
(the changes are in LibOrders.sol)facets/OwnerFacet.sol
(mostly renames, new set functions)facets/SecondaryLiquidationFacet.sol
(minShortErc, renamed, recoveryMode)facets/ShortRecordFacet.sol
(minShortErc, recoveryMode)facets/TWAPFacet.sol
facets/YieldFacet.sol
facets/VaultFacet.sol
facets/ViewFacet.sol
libraries/LibShortRecord.sol
libraries/LibVault.sol
(new withdraw methods)libraries/AppStorage.sol
(holds structs)libraries/Constants.sol
libraries/DataTypes.sol
(struct packing)libraries/LibAsset.sol
libraries/LibBridge.sol
(only removal)libraries/PRBMathHelper.sol
(also out of scope previously)facets/DiamondCutFacet.sol
(unchanged)facets/DiamondEtherscanFacet.sol
(new, but view only)facets/DiamondLoupeFacet.sol
(unchanged)facets/TestFacet.sol
governance/DittoGovernor.sol
(also out of scope previously)governance/DittoTimelockController.sol
(also out of scope previously)interfaces/*.sol
libraries/console.sol
libraries/Errors.sol
libraries/Events.sol
libraries/LibDiamond.sol
(old version had bug with removing functions in 0.8.20)libraries/LibDiamondEtherscan.sol
libraries/UniswapOracleLibrary.sol
(also out of scope previously)libraries/UniswapTickMath.sol
(also out of scope previously)mocks/*.sol
tokens/Asset.sol
(no change, ERC)tokens/Ditto.sol
(no change, ERC)Diamond.sol
(unchanged)EtherscanDiamondImpl.sol
(used for etherscan)This is large update to the original codebase, so the scope doesn't encompass everything that will be relevant to review it. Most of the changes are to address previously known issues or findings from the last audit (if you want to review previous findings, you can filter by "finding-x" in the Codehawk submissions).
minBidEth
, minAskEth
, minShortErc
which was checked during the start of createBid
,createAsk
, createLimitShort
calls.Contract1
: Should comply with ERC/EIPX
Contract2
: Should comply with ERC/EIPY
minShortErc
: Primary liquidators should always have a large enough incentive to liquidate (callerFeePct
tied to liquidated collateral) risky debt because every ShortRecord must either contain enough ercDebt or have access to enough ercDebt (through cancelling the associated short order). The one noted exception is listed in known issues (ercDebt requirements met from application of ercDebtRate)Describe the project's main invariants (properties that should NEVER EVER be broken).
See docs for more info.
Ditto's orderbook acts similar to central limit orderbook with some changes. In order to make the gas costs low, there is a hint system added to enable a user to place an order in the orderbook mapping. Asks/Shorts are both on the "sell" side of the orderbook. Order structs are reused by implementing the orders as a doubly linked-list in a mapping. HEAD
order is used as a starting point to match against.
HEAD.prevId
) these are the only possible OrderTypes: O.Matched
, O.Cancelled
, O.Uninitialized
).orderId
counter, every single orderId
should be uniqueshortOrders
can only be limit orders. startingShort
represents the first short order that can be matched. Normally HEAD.nextId
would the next short order in the mapping, but it's not guaranteed that it is matchable since users can still create limit shorts under the oracle price (or they move below oracle once the price updates). Oracle updates from chainlink or elsewhere will cause the startingShort
to move, which means the system doesn't know when to start matching from without looping through each short, so the system allows a temporary matching backwards.
shortOrder
can't match under oraclePrice
startingShort
price must be greater than or equal to oraclePrice
shortOrder
with a non-zero (ie. positive) shortRecordId
means that the referenced SR is status partialFillShortRecords are the Vaults/CDPs/Troves of Ditto. SRs represent a collateral/debt position by a shorter. Each user can have multiple SRs, which are stored under their address as a list.
shortRecord
debt can be below minShortErc
is when it's partially filled and the connected shortOrder
has enough ercDebt
to make up the difference to minShortErc
(Technically: SR.status
== PartialFill
&& shortOrder.ercAmount
+ ercDebt
>= minShortErc
)SR.FullyFilled
and be under minShortErc
FullyFilled
SR can never have 0 collateralClosed
can ever be re-used (Technically, only SR.Closed
on the left (prevId) side of HEAD, with the exception of HEAD itself)Allows dUSD holders to get equivalent amount of ETH back, akin to Liquity. However the system doesn't automatically sort the SR's lowest to highest. Instead, users propose a list of SRs (an immutable slate) to redeem against. There is a dispute period to revert any proposal changes and a corresponding penalty against a proposer if incorrect. Proposers can claim the ETH after the time period, and shorters can also claim any remaining collateral afterwards.
proposedData
dataTypes should be under 2 CRercDebt
of SR cannot be zero. After proposal, check it cannot be `SR.Closed`` until claimedcollateral
and ercDebt
amounts. The sum total should always add up to the original amounts (save those amounts)Because the Vault mixes rETH/stETH, a credit system is introduced to allow users to withdraw only what they deposit, anything in excess (due to yield) also checks either LSTs price difference using a TWAP via Uniswap.
- If you have a public code repo, please share it here: Previous archive is https://github.com/Cyfrin/2023-09-ditto - How many contracts are in scope?: 12 - Total SLoC for these contracts?: 1961 - How many external imports are there?: 8 - How many separate interfaces and struct definitions are there for the contracts within scope?: Every contract has a generated interface from a script given use of Diamond. 8 Structs are in contracts/libraries/DataTypes.sol in STypes (Storage Types): Order, ShortRecord, NFT, Asset, Vault, AssetUser, VaultUser, Bridge - Does most of your code generally use composition or inheritance?: Composition: mostly Diamond Facets and Libraries - How many external calls?: 18 - What is the overall line coverage percentage provided by your tests?: 96% - Is this an upgrade of an existing system?: True - Check all that apply (e.g. timelock, NFT, AMM, ERC20, rollups, etc.): Ditto token and Stable Assets like dUSD are ERC20s, ShortRecords can become ERC721s - Is there a need to understand a separate part of the codebase / get context in order to audit this part of the protocol?: True, since not everything is in scope - Please describe required context: - Does it use an oracle?: Chainlink ETH/USD and Uniswap stETH and RETH - Describe any novel or unique curve logic or mathematical models your code uses: No - Is this either a fork of or an alternate implementation of another project?: False - Does it use a side-chain?: No - Describe any specific areas you would like addressed: OrderBook logic, issues with dust, redemptions, underwater ShortRecords, also below
removed extra ui/subgraph code
git clone https://github.com/code-423n4/2024-03-dittoeth cd 2024-03-dittoeth # For node: use volta to get node/npm curl https://get.volta.sh | bash volta install node # Use Bun to run TypeScript curl -fsSL https://bun.sh/install | bash # download files from package.json into node_modules npm install # Install foundry for solidity curl -L https://foundry.paradigm.xyz | bash # project as a `npm run prebuild` check for foundry version foundryup -v nightly-5b7e4cb3c882b28f3c32ba580de27ce7381f415a # .env for tests echo 'ANVIL_9_PRIVATE_KEY=0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6' >> .env echo 'MAINNET_RPC_URL=http://eth.drpc.org' >> .env # create interfaces (should already be committed into `interfaces/`, but usually in .gitignore) bun run interfaces-force # build bun run build # unit/fork/invariant tests bun run test # gas tests, check `/.gas.json` bun run test-gas # invariant tests only bun run invariant # run coverage from scratch bun run coverage # view coverage as is (brew install lcov) genhtml \ --output-directory coverage \ filtered-lcov.info open coverage/index.html
bun run
to check commandsgit clean -xfd
bun run anvil-fork
, then deploy with bun run deploy-local
bun run interfaces
to re-compile solidity interfaces to interfaces/
bun run build
to compile contracts, foundry cache in foundry/
tests are located in /test/
and test-gas/
: contains unit, fork, gas, invariant tests
bun run test
-- --vv
for verbosity-- --watch
to watch files-- -m testX
to match testsbun run test-fork
: gas fork tests read from MAINNET_RPC_URL
bun run test-gas
for gas tests, reads test-gas/
, writes gas to .gas.json
bun run coverage
(first brew install lcov
)https://book.getfoundry.sh/forge/writing-tests.html#writing-tests For info on
v
, https://book.getfoundry.sh/forge/tests.html?highlight=vvvv#logs-and-traces
If you get an error like:
Error (9582): Member "asdf" not found or not visible after argument-dependent lookup in contract IDiamond.
It means you need to rebuild the interfaces/
and run bun run interfaces-force
, which generates interfaces/Interface.sol
which are imported in the contracts/
If there's an error with fork tests due to RPC, may need to disable those tests or switch RPC to local node or a different one via changing the .env
like so: MAINNET_RPC_URL=https://eth.drpc.org
. I would suggest any at Chainlist.
[FAIL. Reason: setup failed: Could not instantiate forked environment with fork url [FAIL. Reason: setup failed: backend: failed while inspecting: Database error
Aliases:
alias i='bun run interfaces-force' alias t="forge test " alias tm="forge test --match-test " alias ts="forge test --match-test statefulFuzz" alias g="bun run test-gas" alias gm="FOUNDRY_PROFILE=gas forge build && FOUNDRY_PROFILE=testgas forge test --match-test " alias w='forge test -vv --watch ' # t -m testA # gm testA # w -m testA