This repository contains a compact collateralized debt position (CDP) stablecoin implementation and a test-suite (Foundry). The system is intentionally minimal for learning and experimentation. The project implements multi-collateral positions, user-chosen collateralization ratios, liquidation, and a protocol treasury that accrues fees from liquidations.
Quickstart
- Install Foundry: https://book.getfoundry.sh/
- Install test helpers:
forge install foundry-rs/forge-std- Run tests:
forge test -vCore Contracts
src/CDPStablecoin.sol: The primary contract. It implements an ERC20 stablecoin (the protocol mintscUSD) and the CDP logic (deposit, withdraw, mint, repay, liquidation, treasury bookkeeping and admin controls).src/MockOracle.sol: A simple Chainlink-style mock aggregator used by tests.src/SimpleERC20*.sol: Test tokens (different decimals) used in the test-suite.
Recent design highlights
- OpenZeppelin-style dependencies vendored locally: The CDP contract now uses OpenZeppelin-compatible
ERC20(vendored here as a compact implementation),Ownable, andReentrancyGuardsemantics. These live underlib/openzeppelin-contracts/to keep the project self-contained for testing. - Treasury merged into CDP contract: Seized fees from liquidations are tracked in
treasuryBalance[token](mapping of token address to amount). This avoids an externalTreasurycontract in tests and provides owner controls directly on the CDP contract.
Key workflows & semantics
- Deposit collateral:
depositCollateral(token, amount)— caller mustapprovethe CDP contract first. - Set collateralization ratio:
setCollateralizationRatio(token, percent)— users choose their own ratio but it must be >=MIN_COLLATERAL_RATIO(110%). - Mint stablecoin:
mint(token, amount)— mintscUSDup to the allowed amount given the user's collateral and chosen ratio. - Repay / burn:
repay(amount)— pullscUSDfrom the caller and burns it, reducing per-token debts proportionally. - Liquidation:
liquidate(user, token, repayAmount)— when a user's health ratio for that token position falls belowLIQUIDATION_THRESHOLD(100%), a liquidator can repay part of the user's debt and receive collateral. A small fee percent of collateral taken is accrued intotreasuryBalance[token].
Math note (units):
- All internal USD computations are normalized to 18 decimals.
- Token decimals are read from token contracts (test tokens expose
decimals()or a publicdecimalsgetter to be compatible with the helpers).
Liquidation math (summary):
- Given
repayAmount(cUSD, 18 decimals) the contract calculates how many collateral tokens are needed (using the oracle price normalized to 18 decimals), adds theLIQUIDATION_BONUS(liquidator incentive), then computes theLIQUIDATION_FEEportion to keep in the protocol treasury. The liquidator receives the collateral minus the fee; the fee is accounted intreasuryBalance[token].
Events
Liquidated(address indexed user, address indexed token, uint256 repaid, uint256 collateralTaken, uint256 treasuryAmount)repaid: stablecoin amount the liquidator repaid and that was burned.collateralTaken: gross collateral removed from the user's position (includes bonus to liquidator).treasuryAmount: amount of the seized collateral assigned to protocol treasury (kept inside the CDP contract astreasuryBalance[token]).
Admin / Owner functions
pause()/unpause()— pause protocol actions that are guarded by thenotPausedmodifier.setBlacklist(address who, bool v)— mark accounts as blacklisted (prevents certain operations).addSupportedToken(token, aggregator)/removeSupportedToken(token)— extend or shrink supported collateral tokens.withdrawTreasury(token, to, amount)— withdraw protocol treasury-held collateral fortoken(onlyOwner). This reducestreasuryBalance[token].emergencyWithdraw(token, to, amount)— owner may withdraw tokens that are held by the contract but not reserved for the treasury (i.e.,balanceOf(this) - treasuryBalance[token]). This is useful to recover stray tokens without touching treasury accounting.- Ownership management is provided by the vendored
Ownable(transferOwnership,renounceOwnership).
Helper views
estimatedLiquidationPrice(user, token) -> price18— returns the oracle price (normalized to 18 decimals) at which the givenuser'stokenposition would reach theLIQUIDATION_THRESHOLDgiven current collateral amount and outstanding debt. Returns0when there is no collateral or debt.
Testing notes
- Tests are in
test/and exercise typical flows: decimals combinations, liquidation fuzzing, rounding/dust edge cases, and the global liquidation simulation. The tests useMockOracleand localSimpleERC20tokens so they are deterministic.
How to extend or change liquidation economics
- To change the split between liquidator and treasury (for example, have the treasury receive the majority and give liquidator only a small
1% bonus), update the
_computeLiquidationInfohelper insrc/CDPStablecoin.soland adjust tests that assert exact balances or treasury accruals.
Security & audit notes
- This is intentionally small and educational. Real deployments require:
- Robust oracle protections (staleness, manipulation resistance).
- More sophisticated liquidation mechanics (partial liquidations, auction, incentives tuning).
- Safe math beyond Solidity's built-ins where precision matters, and careful edge-case testing for rounding.
- A governance model and timelocks for sensitive admin operations.