1 DSC tracks 1 USD, backed by WETH and WBTC at a 200% collateral ratio with a 10% liquidation bonus. Algorithmically stabilized — no governance, no fees, no protocol-owned treasury. Foundry unit + paired invariant suites, Slither in CI, Sepolia deployment with verified contracts.
Closer in spirit to single-purpose MakerDAO without DAI's surface area.
| Property | Choice |
|---|---|
| Peg | USD (1 DSC = $1) |
| Collateral | Exogenous (crypto) — WETH (18 dec), WBTC (8 dec) |
| Stability mechanism | Minting / burning, algorithmically decentralized — users may only mint with enough collateral |
| Price feeds | Chainlink, staleness-checked, with on-chain ETH/BTC ↔ USD conversion helpers |
| Relative stability | Anchored / pegged to the US Dollar |
| Collateral ratio | 200% |
| Liquidation bonus | 10% |
Users deposit collateral and mint DSC against it. If a position's collateral value falls below 200% of its debt, other users may liquidate it by burning DSC on the user's behalf and seizing the same USD value in collateral plus an extra 10% as a bonus — this bonus is the only incentive keeping the currency collateralized, since there is no automated keeper. Self-liquidation is permitted, and a user who liquidates herself does not lose the 10% bonus to a third party.
flowchart LR
User([User / Liquidator])
Engine[DSCEngine]
DSC[DecentralizedStableCoin<br/>ERC20]
Oracle[OracleLib]
Chainlink[(Chainlink<br/>ETH/USD · BTC/USD)]
User -->|deposit · mint · burn · redeem · liquidate| Engine
Engine -->|owns · sole minter| DSC
Engine -.->|using OracleLib for AggregatorV3Interface| Oracle
Oracle -->|staleCheckLatestRoundData| Chainlink
Oracle -. revert on stale / zero / backwards round .-> Engine
Three on-chain contracts in a single ownership chain:
DSCEngine— entry point for deposit, mint, burn, redeem, liquidate. Custodies collateral, tracks per-user debt, computes the health factor that gates every state-changing call. ExposesgetUsdValueandgetTokenAmountFromUsdas the ETH/BTC ↔ USD conversion surface.DecentralizedStableCoin— the ERC20 thatDSCEnginemints. Ownership is transferred to the engine atomically during deployment, so the engine is the sole minter for the lifetime of the protocol.OracleLib—using-attached Chainlink wrapper. Reverts on stale or backwards round data, intentionally freezing the protocol rather than transacting on degraded prices.
- State-before-transfer ordering. Every public entry point updates storage, then performs the ERC20 call, then closes with a health-factor check on the caller. The private helpers (
_redeemCollateral,_burnDsc) exist specifically to enforce this ordering at the public boundary. - Multi-decimal collateral on one math path. WBTC (8 dec) and WETH (18 dec) share the engine's valuation logic via per-token decimal scaling applied at deposit accounting and at oracle normalization. Tokens with more than 18 decimals are rejected at construction.
- Oracle freeze-on-stale. A single library wraps every Chainlink read. A stale, zero, or out-of-order round halts every priced operation — the protocol prefers freezing to serving wrong prices.
- Failure-path mocks. A
MockDSC*family deliberately returnsfalsefrommint/transfer/transferFrom, and one variant crashes the oracle mid-burn. These reach the engine's defensiveif (!success) revertand oracle-collapse branches that would otherwise be untestable. - Two-flavor invariant fuzzing. A strict
FailOnRevertHandler(every bounded call must succeed) and a lenientContinueOnRevertHandler(broader state exploration with reverts tolerated) drive the same two solvency invariants:- Total collateral USD value ≥ DSC total supply.
- Σ per-user
dscMintedequalstotalSupply.
- Slither in CI. Configured to fail the build on
highfindings, with inline// slither-disable-next-linesuppressions justified one comment above the disabled line. - Chain-keyed deploy + auto-discovering scripts.
HelperConfigswitches between Anvil mocks and Sepolia's live Chainlink feeds byblock.chainid. Per-action interaction scripts resolve the latest deployed addresses viaDevOpsTools, so re-deploying and then calling any action "just works" without passing addresses around.
forge test runs three tiers under test/:
unit/— happy paths and revert-expecting paths, including the failure-path mock suite.invariant/— paired strict + lenient runners against a stateful handler that tracks fuzzed users in anEnumerableSet.- Sepolia and mainnet fork variants of the unit suite via dedicated Makefile targets.
Line coverage on src/ is 100%, measured via make coverage (forge coverage scoped to production sources, invariant runs excluded).
CI runs forge fmt --check, forge build --sizes, the unit suite, the invariant suite, and Slither on every pull request (and on pushes to main/release/develop).
- Deposit collateral and mint DSC
- Redeem collateral for DSC
- Deposit collateral
- Redeem collateral
- Mint DSC
- Burn DSC
make install # forge soldeer install
make build
make test # unit + invariant
make slither
make deploy-sepoliaSee the Makefile for the full target list (per-action interaction scripts, coverage, fork tests).
Builds on the Cyfrin Advanced Foundry teaching codebase by Patrick Collins / Cyfrin (repo, CodeHawks audit), with adaptations (OpenZeppelin 5.x, solc 0.8.27, Slither hygiene) and one substantive extension: per-token decimal scaling so 8-dec WBTC and 18-dec WETH share one valuation path. The invariant suite, the failure-path mock family, and the Sepolia deployment are additions on top of the upstream contracts.