引言在DeFi生态中,抵押债务头寸(CDP,CollateralizedDebtPosition)是构建去中心化稳定币的核心机制。MakerDAO首创的DAI模式证明了通过超额抵押加密资产生成稳定币的可行性。本文将基于您提供的完整代码,详细解析如何构建一个现代化的DAI协议
在 DeFi 生态中,抵押债务头寸(CDP, Collateralized Debt Position) 是构建去中心化稳定币的核心机制。MakerDAO 首创的 DAI 模式证明了通过超额抵押加密资产生成稳定币的可行性。本文将基于您提供的完整代码,详细解析如何构建一个现代化的 DAI 协议——ModernDAI,涵盖合约设计、测试策略与部署流程。 ModernDAI 展示了一个功能完整的 CDP 稳定币协议的核心实现;
一、DAI vs USDT/USDC 核心区别
概述:USDT/USDC是"数字美元"(方便但中心化),DAI是"加密世界的美元"(自由但复杂)
| 维度 | DAI | USDT/USDC |
|---|---|---|
| 抵押物 | 加密货币(ETH等) | 美元现金/美债 |
| 发行方 | 智能合约协议 | Tether/Circle公司 |
| 透明度 | 链上实时可查 | 依赖第三方审计 |
| 冻结风险 | 无法被冻结 | 发行方可冻结地址 |
ModernDAI 实现了 CDP 稳定币的四大核心功能模块:
| 模块 | 功能描述 | 关键参数 |
|---|---|---|
| CDP 管理 | 超额抵押 ETH 铸造 DAI | 清算比率 150%,清算罚金 10% |
| 稳定费系统 | 按时间累积的借款利息 | 可治理调整的年化费率 |
| DAI 储蓄率 (DSR) | 持币生息机制 | 复利计算,实时累积 |
| 预言机喂价 | 抵押品价格更新 | ORACLE_ROLE 权限控制 |
contract ModernDAI is
ERC20, // 基础代币功能
ERC20Burnable, // 销毁机制
Nonces, // EIP-712 防重放
ERC20Permit, // 离线授权(gasless)
AccessControl, // 角色权限管理
Pausable, // 紧急暂停
ReentrancyGuard // 重入攻击防护
设计亮点:采用 OpenZeppelin 标准库构建,通过 AccessControl 实现精细权限管理(MINTER / ORACLE / PAUSER / ADMIN)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract ModernDAI is
ERC20,
ERC20Burnable,
Nonces,
ERC20Permit,
AccessControl,
Pausable,
ReentrancyGuard
{
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
uint256 public constant PRECISION = 1e18;
uint256 public debtCeiling;
uint256 public stabilityFee;
uint256 public liquidationRatio = 1.5e18;
uint256 public liquidationPenalty = 0.1e18;
uint256 public price = 2000e18;
uint256 public savingsRate;
uint256 public savingsAccumulator = PRECISION;
uint256 public lastSavingsUpdate;
mapping(address => uint256) public savingsPrincipal;
struct CDP {
uint256 collateral;
uint256 debt;
uint256 lastUpdate;
}
mapping(address => CDP) public cdps;
event CDPOpened(address indexed user, uint256 collateral, uint256 debt);
event Liquidated(address indexed user, uint256 seizedCollateral, uint256 debtRepaid);
constructor(string memory name, string memory symbol, uint256 _debtCeiling)
ERC20(name, symbol)
ERC20Permit(name)
{
debtCeiling = _debtCeiling;
lastSavingsUpdate = block.timestamp;
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
_grantRole(ORACLE_ROLE, msg.sender);
_grantRole(PAUSER_ROLE, msg.sender);
}
// ============ 新增/补全的 Setter 函数 (解决测试报错的关键) ============
function setStabilityFee(uint256 _fee) external onlyRole(DEFAULT_ADMIN_ROLE) {
stabilityFee = _fee;
}
function setSavingsRate(uint256 _rate) external onlyRole(DEFAULT_ADMIN_ROLE) {
_updateSavingsAccumulator();
savingsRate = _rate;
}
function setDebtCeiling(uint256 _ceiling) external onlyRole(DEFAULT_ADMIN_ROLE) {
debtCeiling = _ceiling;
}
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
// ============ 核心功能 ============
function openCDP(uint256 daiToBorrow) external payable nonReentrant whenNotPaused {
require(msg.value > 0, "No collateral");
CDP storage cdp = cdps[msg.sender];
if (cdp.debt > 0) {
cdp.debt = _calculateDebtWithFee(cdp.debt, cdp.lastUpdate);
}
cdp.collateral += msg.value;
cdp.debt += daiToBorrow;
cdp.lastUpdate = block.timestamp;
uint256 colValue = (cdp.collateral * price) / PRECISION;
require((colValue * PRECISION) / cdp.debt >= liquidationRatio, "Undercollateralized");
require(totalSupply() + daiToBorrow <= debtCeiling, "Debt ceiling reached");
_mint(msg.sender, daiToBorrow);
emit CDPOpened(msg.sender, msg.value, daiToBorrow);
}
function liquidate(address user) external nonReentrant {
CDP storage cdp = cdps[user];
uint256 currentDebt = _calculateDebtWithFee(cdp.debt, cdp.lastUpdate);
uint256 colValue = (cdp.collateral * price) / PRECISION;
require((colValue * PRECISION) / currentDebt < liquidationRatio, "CDP is safe");
uint256 debtToRepay = currentDebt + (currentDebt * liquidationPenalty) / PRECISION;
uint256 collateralToSeize = (debtToRepay * PRECISION) / price;
if (collateralToSeize > cdp.collateral) collateralToSeize = cdp.collateral;
_burn(msg.sender, currentDebt);
cdp.collateral -= collateralToSeize;
delete cdps[user];
(bool success, ) = payable(msg.sender).call{value: collateralToSeize}("");
require(success, "ETH transfer failed");
emit Liquidated(user, collateralToSeize, currentDebt);
}
function depositSavings(uint256 amount) external nonReentrant {
_updateSavingsAccumulator();
_transfer(msg.sender, address(this), amount);
uint256 shares = (amount * PRECISION) / savingsAccumulator;
savingsPrincipal[msg.sender] += shares;
}
function withdrawSavings(uint256 shares) external nonReentrant {
_updateSavingsAccumulator();
require(savingsPrincipal[msg.sender] >= shares, "Insufficient savings");
uint256 amountToReturn = (shares * savingsAccumulator) / PRECISION;
savingsPrincipal[msg.sender] -= shares;
uint256 contractBal = balanceOf(address(this));
if (contractBal < amountToReturn) {
_mint(address(this), amountToReturn - contractBal);
}
_transfer(address(this), msg.sender, amountToReturn);
}
function _updateSavingsAccumulator() internal {
if (block.timestamp > lastSavingsUpdate) {
uint256 timeElapsed = block.timestamp - lastSavingsUpdate;
uint256 interest = (savingsRate * timeElapsed) / 365 days;
savingsAccumulator += (savingsAccumulator * interest) / PRECISION;
lastSavingsUpdate = block.timestamp;
}
}
function nonces(address owner) public view virtual override(ERC20Permit, Nonces) returns (uint256) {
return super.nonces(owner);
}
function _calculateDebtWithFee(uint256 debt, uint256 lastUpdate) internal view returns (uint256) {
uint256 timeElapsed = block.timestamp - lastUpdate;
if (timeElapsed == 0) return debt;
uint256 fee = (debt * stabilityFee * timeElapsed) / (365 days * PRECISION);
return debt + fee;
}
function setPrice(uint256 _price) external onlyRole(ORACLE_ROLE) {
price = _price;
}
function pause() external onlyRole(PAUSER_ROLE) { _pause(); }
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { _unpause(); }
receive() external payable {}
}
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { parseEther, slice, hexToNumber } from "viem";
import { network } from "hardhat";
describe("ModernDAI Full Protocol Integration", function () {
async function deployFixture() {
const { viem } = await (network as any).connect();
const [admin, user, liquidator] = await viem.getWalletClients();
const publicClient = await viem.getPublicClient();
const testClient = await viem.getTestClient();
const debtCeiling = parseEther("1000000");
const dai = await viem.deployContract("ModernDAI", [
"Modern Dai",
"DAI",
debtCeiling,
]);
// 现在合约里有这些函数了,不会再报 AbiFunctionNotFoundError
await dai.write.setStabilityFee([parseEther("0.05")], { account: admin.account });
await dai.write.setSavingsRate([parseEther("0.1")], { account: admin.account });
return { dai, admin, user, liquidator, publicClient, testClient };
}
describe("1. 基础权限", function () {
it("应该正确初始化", async function () {
const { dai } = await deployFixture();
assert.equal(await dai.read.name(), "Modern Dai");
});
});
describe("2. CDP 业务", function () {
it("抵押并借款", async function () {
const { dai, user } = await deployFixture();
await dai.write.openCDP([parseEther("1000")], {
account: user.account,
value: parseEther("1"),
});
const cdp = await dai.read.cdps([user.account.address]);
assert.equal(cdp[0], parseEther("1"));
});
});
describe("3. DSR 储蓄", function () {
it("产生利息", async function () {
const { dai, user, testClient } = await deployFixture();
await dai.write.openCDP([parseEther("1000")], { account: user.account, value: parseEther("2") });
await dai.write.depositSavings([parseEther("1000")], { account: user.account });
await testClient.increaseTime({ seconds: 31536000 });
await testClient.mine({ blocks: 1 });
const shares = await dai.read.savingsPrincipal([user.account.address]);
await dai.write.withdrawSavings([shares], { account: user.account });
const bal = await dai.read.balanceOf([user.account.address]);
assert.ok(bal >= parseEther("1100"));
});
});
describe("4. Permit 签名", function () {
it("离线授权", async function () {
const { dai, user, admin, publicClient } = await deployFixture();
const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);
const nonce = await dai.read.nonces([user.account.address]);
const chainId = BigInt(await publicClient.getChainId());
const signature = await user.signTypedData({
domain: { name: "Modern Dai", version: "1", chainId, verifyingContract: dai.address },
types: {
Permit: [
{ name: "owner", type: "address" }, { name: "spender", type: "address" },
{ name: "value", type: "uint256" }, { name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
},
primaryType: "Permit",
message: { owner: user.account.address, spender: admin.account.address, value: 500n, nonce, deadline },
});
const r = slice(signature, 0, 32);
const s = slice(signature, 32, 64);
const v = hexToNumber(slice(signature, 64, 65));
await dai.write.permit([user.account.address, admin.account.address, 500n, deadline, v, r, s]);
assert.equal(await dai.read.allowance([user.account.address, admin.account.address]), 500n);
});
});
});
// scripts/deploy.js
import { network, artifacts } from "hardhat";
import { parseEther } from "viem";
async function main() {
// 连接网络
const { viem } = await network.connect({ network: network.name });//指定网络进行链接
// 获取客户端
const [deployer] = await viem.getWalletClients();
const publicClient = await viem.getPublicClient();
const deployerAddress = deployer.account.address;
console.log("部署者的地址:", deployerAddress);
// 加载合约
const DAIArtifact = await artifacts.readArtifact("contracts/DAI.sol:ModernDAI");
const debtCeiling = parseEther("1000");
const DAIHash = await deployer.deployContract({
abi: DAIArtifact.abi,//获取abi
bytecode: DAIArtifact.bytecode,//硬编码
args: [ "Modern Dai",
"DAI", debtCeiling],
});
const DAIReceipt = await publicClient.waitForTransactionReceipt({ hash: DAIHash });
console.log("DAI合约地址:", DAIReceipt.contractAddress);
}
main().catch(console.error);
本文从理论到实践,完整呈现了DAI智能合约的全貌。通过多维度对比USDT/USDC,深入剖析了DAI的去中心化优势与潜在风险,并系统阐述了其架构设计。最终,项目顺利完成开发、测试与部署,实现了从概念到落地的完整闭环。DAI作为去中心化稳定币的代表,其技术路径为DeFi生态的发展提供了重要参考。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!