从零构建 ModernDAI:基于 Hardhat + Viem 的 CDP 稳定币协议开发实战

  • 木西
  • 发布于 2小时前
  • 阅读 15

引言在DeFi生态中,抵押债务头寸(CDP,CollateralizedDebtPosition)是构建去中心化稳定币的核心机制。MakerDAO首创的DAI模式证明了通过超额抵押加密资产生成稳定币的可行性。本文将基于您提供的完整代码,详细解析如何构建一个现代化的DAI协议

引言

在 DeFi 生态中,抵押债务头寸(CDP, Collateralized Debt Position) 是构建去中心化稳定币的核心机制。MakerDAO 首创的 DAI 模式证明了通过超额抵押加密资产生成稳定币的可行性。本文将基于您提供的完整代码,详细解析如何构建一个现代化的 DAI 协议——ModernDAI,涵盖合约设计、测试策略与部署流程。 ModernDAI 展示了一个功能完整的 CDP 稳定币协议的核心实现;

一、DAI vs USDT/USDC 核心区别

概述:USDT/USDC是"数字美元"(方便但中心化),DAI是"加密世界的美元"(自由但复杂)

1.1 DAI vs USDT/USDC多维度对比表格

维度 DAI USDT/USDC
抵押物 加密货币(ETH等) 美元现金/美债
发行方 智能合约协议 Tether/Circle公司
透明度 链上实时可查 依赖第三方审计
冻结风险 无法被冻结 发行方可冻结地址

1.2 DAI的三大优势

  1. 去中心化 — 无公司控制,抗审查,不怕银行倒闭
  2. 链上透明 — 所有抵押品和债务实时可见,无需信任第三方
  3. DeFi原生 — 无缝集成各类协议,可通过DSR直接赚取收益

1.3 主要缺点

  • 需要150%+超额抵押,资金效率低
  • 智能合约有代码漏洞风险
  • 加密暴跌时可能来不及清算
    • *

      二、协议架构设计

      2.1 核心机制概览

ModernDAI 实现了 CDP 稳定币的四大核心功能模块:

模块 功能描述 关键参数
CDP 管理 超额抵押 ETH 铸造 DAI 清算比率 150%,清算罚金 10%
稳定费系统 按时间累积的借款利息 可治理调整的年化费率
DAI 储蓄率 (DSR) 持币生息机制 复利计算,实时累积
预言机喂价 抵押品价格更新 ORACLE_ROLE 权限控制

2.2 合约继承结构

contract ModernDAI is 
    ERC20,           // 基础代币功能
    ERC20Burnable,   // 销毁机制
    Nonces,          // EIP-712 防重放
    ERC20Permit,     // 离线授权(gasless)
    AccessControl,   // 角色权限管理
    Pausable,        // 紧急暂停
    ReentrancyGuard  // 重入攻击防护

设计亮点:采用 OpenZeppelin 标准库构建,通过 AccessControl 实现精细权限管理(MINTER / ORACLE / PAUSER / ADMIN)

三、智能合约开发、测试、部署一站式

3.1 智能合约

// 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 {}
}

3.2 测试脚本

测试用例

  1. 基础权限:应该正确初始化 (1435ms)
  2. CDP 业务:抵押并借款 (635ms)
  3. DSR 储蓄: 产生利息 (644ms)
  4. Permit 签名:离线授权 (628ms)
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);
        });
    });
});

3.3 部署脚本

// 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生态的发展提供了重要参考。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。