深度复刻 Sky Protocol:基于 OpenZeppelin V5 与 Solidity 0.8.24 的工程实践

前言

随着 MakerDAO 正式升级为 Sky ,其"Endgame"计划展示了一个复杂的去中心化金融闭环:通过 USDS 稳定币、SKY 治理代币以及 sUSDS 储蓄模块实现价值捕获。本文将从架构设计到自动化测试,手把手教你如何复刻这一顶级 DeFi 协议。

核心看点:模块化架构+权限分层+Ray精度利率算法,这套设计被业内称为"DeFi工程化的新标杆"。

一、 核心架构:双代币与权限引擎

Sky Protocol 的核心不再是传统的单体合约,而是一个高度模块化的系统。

1. 资产层 (Assets)

我们使用了 OpenZeppelin V5 的 ERC20Permit

  • USDS : 系统本位币。引入 Permit 允许用户通过签名授权,实现"无 Gas"存款体验。
  • SKY : 治理代币。它通过 AccessManaged 挂载到权限管理器上,确保只有合法的转换器(Migrator)或治理模块可以铸造。

2. 权限层 (The Brain)

这是复刻成功的关键。我们放弃了旧版的 ds-auth,转而采用 OZ V5 AccessManager。它支持:

  • 角色基础访问控制 (RBAC)
  • 执行延迟 (Delay) :为关键操作(如修改利率)设置观察期,增加安全性。

二、 关键技术实现:sUSDS 利率算法

Sky 的储蓄收益(SSR)使用了 Ray 算术(27位精度) 。在 sUSDS.sol 中,我们通过继承 ERC4626 标准实现了代币化仓位。

核心公式

资产的增长基于时间线性累积:

<math xmlns="http://www.w3.org/1998/Math/MathML"> T o t a l A s s e t s = U n d e r l y i n g A s s e t s × R A Y + ( S S R − R A Y ) × Δ t 365 d a y s × R A Y TotalAssets = UnderlyingAssets \times \frac{RAY + (SSR - RAY) \times \Delta t}{365 days \times RAY} </math>TotalAssets=UnderlyingAssets×365days×RAYRAY+(SSR−RAY)×Δt

这种设计确保了即便在高并发提现的情况下,每一位用户的利息计算都能精确到秒。


三、 工程化实战:使用 Viem 进行集成测试

1. 核心智能合约

  • 访问管理器(SkyAccessManager)
js 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/access/manager/AccessManager.sol";

// 这样 Hardhat 编译器就会处理 AccessManager 并生成 Artifact
contract SkyAccessManager is AccessManager {
    constructor(address initialAdmin) AccessManager(initialAdmin) {}
}
  • 系统本位币(USDS)
js 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/access/manager/AccessManaged.sol";

contract USDS is ERC20, ERC20Permit, AccessManaged {
    constructor(address initialAuthority) 
        ERC20("Sky Dollar", "USDS") 
        ERC20Permit("Sky Dollar")
        AccessManaged(initialAuthority) 
    {}

    // 仅限受控角色(如 Migrator 或 Lending 模块)铸造
    function mint(address to, uint256 amount) public restricted {
        _mint(to, amount);
    }

    function burn(address from, uint256 amount) public restricted {
        _burn(from, amount);
    }
}
  • 治理代币(SKY)
js 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/manager/AccessManaged.sol";

// 确保这里的名称是 SKY,而不是 SkyToken 或其他
contract SKY is ERC20, AccessManaged {
    constructor(address initialAuthority) 
        ERC20("Sky Token", "SKY") 
        AccessManaged(initialAuthority) 
    {}

    function mint(address to, uint256 amount) public restricted {
        _mint(to, amount);
    }
}
  • 储蓄池(sUSDS)
js 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import "@openzeppelin/contracts/access/manager/AccessManaged.sol";
contract sUSDS is ERC4626, AccessManaged {
    uint256 public constant RAY = 1e27;
    uint256 public ssr; // 年化利率 (Ray 精度)
    uint256 public rho; // 上次更新时间

    constructor(IERC20 asset, address initialAuthority) 
        ERC4626(asset) 
        ERC20("Savings USDS", "sUSDS")
        AccessManaged(initialAuthority)
    {
        ssr = RAY; // 初始 0% 收益
        rho = block.timestamp;
    }

    // 覆盖以计算包含利息后的总资产
    function totalAssets() public view override returns (uint256) {
        uint256 underlyingAssets = super.totalAssets();
        uint256 timeElapsed = block.timestamp - rho;
        if (timeElapsed == 0 || ssr <= RAY) return underlyingAssets;

        // 简化的线性累积公式
        return (underlyingAssets * (RAY + (ssr - RAY) * timeElapsed / 365 days)) / RAY;
    }

    // 治理更新利率前必须先同步状态
    function setSSR(uint256 newSsr) public restricted {
        // 实际操作中应先进行一次虚拟结算以固定之前收益
        rho = block.timestamp;
        ssr = newSsr;
    }
}
  • 锁仓模块(SkyLockstake)
js 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/manager/AccessManaged.sol";
contract SkyLockstake is AccessManaged, ReentrancyGuard {
    struct Deposit {
        uint256 amount;
        uint256 lockUntil;
    }

    IERC20 public immutable sky;
    mapping(address => Deposit) public deposits;
    uint256 public constant MIN_LOCK_PERIOD = 30 days;

    constructor(IERC20 _sky, address initialAuthority) AccessManaged(initialAuthority) {
        sky = _sky;
    }

    function lock(uint256 amount) external nonReentrant {
        sky.transferFrom(msg.sender, address(this), amount);
        deposits[msg.sender].amount += amount;
        deposits[msg.sender].lockUntil = block.timestamp + MIN_LOCK_PERIOD;
    }

    function withdraw() external nonReentrant {
        Deposit storage d = deposits[msg.sender];
        require(block.timestamp >= d.lockUntil, "Lock period not over");
        uint256 amount = d.amount;
        d.amount = 0;
        sky.transfer(msg.sender, amount);
    }
}

2. 自动化测试闭环

我们编写的测试脚本涵盖了四个维度:Sky Protocol (USDS + sUSDS + Lockstake) Integration

  • 权限与资产基础:只有被授权的角色可以铸造 USDS。
  • 利息模拟 :利用 testClient.increaseTime 模拟一年后的收益,验证 Ray 算术的准确性。
  • 锁定逻辑 :复刻 Sky 的 Lockstake 机制,测试 30 天锁定期的强制执行。
  • 签名授权:Permit 离线授权,USDS 应该支持签名授权存款。
js 复制代码
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { parseEther, slice, hexToNumber, zeroAddress } from "viem";
import { network } from "hardhat";

describe("Sky Protocol (USDS + sUSDS + Lockstake) Integration", function () {
    async function deployFixture() {
        const { viem } = await (network as any).connect();
        const [admin, user, bot] = await viem.getWalletClients();
        const publicClient = await viem.getPublicClient();
        const testClient = await viem.getTestClient();

        // 1. 部署权限管理器 AccessManager (OZ V5)
        const accessManager = await viem.deployContract("SkyAccessManager", [admin.account.address]);

        // 2. 部署资产层
        const usds = await viem.deployContract("USDS", [accessManager.address]);
        const sky = await viem.deployContract("SKY", [accessManager.address]);

        // 3. 部署收益层与锁定层
        const susds = await viem.deployContract("sUSDS", [usds.address, accessManager.address]);
        const lockstake = await viem.deployContract("SkyLockstake", [sky.address, accessManager.address]);

        // 4. 权限配置 (模仿 Sky 治理逻辑)
        // 赋予 Admin MINTER_ROLE 以便测试铸造,实际生产中应给 Migrator
        const MINTER_ROLE = 1n; 
        await accessManager.write.grantRole([MINTER_ROLE, admin.account.address, 0n]);
        // 绑定 USDS.mint 函数到 MINTER_ROLE
        const mintSelector = "0x40c10f19"; // mint(address,uint256)
        await accessManager.write.setTargetFunctionRole([usds.address, [mintSelector], MINTER_ROLE]);

        return { accessManager, usds, sky, susds, lockstake, admin, user, bot, publicClient, testClient };
    }

    describe("1. 权限与资产基础", function () {
        it("只有被授权的角色可以铸造 USDS", async function () {
            const { usds, admin, user } = await deployFixture();
            const amount = parseEther("1000");

            // Admin 拥有权限,可以铸造
            await usds.write.mint([user.account.address, amount], { account: admin.account });
            assert.equal(await usds.read.balanceOf([user.account.address]), amount);

            // User 没有权限,尝试铸造应失败 (AccessManaged 逻辑)
            try {
                await usds.write.mint([admin.account.address, amount], { account: user.account });
                assert.fail("Should have reverted");
            } catch (e: any) {
                assert.ok(e.message.includes("AccessManagedUnauthorized"));
            }
        });
    });

    describe("2. sUSDS (SSR) 利率累积算法", function () {
        it("sUSDS 应该随时间产生线性利息", async function () {
            const { usds, susds, admin, user, testClient } = await deployFixture();
            const RAY = 10n ** 27n;
            const depositAmount = parseEther("1000");

            // 准备资金并存款
            await usds.write.mint([user.account.address, depositAmount], { account: admin.account });
            await usds.write.approve([susds.address, depositAmount], { account: user.account });
            await susds.write.deposit([depositAmount, user.account.address], { account: user.account });

            // 设置 SSR 年化为 10% (1.10 * RAY)
            const newSSR = (RAY * 110n) / 100n;
            await susds.write.setSSR([newSSR], { account: admin.account });

            // 快进 1 年 (31536000 秒)
            await testClient.increaseTime({ seconds: 31536000 });
            await testClient.mine({ blocks: 1 });

            // 检查总资产:1000 + 10% = 1100
            const assets = await susds.read.totalAssets();
            // 允许极小的舍入误差
            assert.ok(assets >= parseEther("1100") && assets < parseEther("1101"));
        });
    });

    describe("3. Lockstake 锁定逻辑", function () {
        it("在锁定期间内不能提前提取 SKY", async function () {
            const { sky, lockstake, admin, user, testClient } = await deployFixture();
            const stakeAmount = parseEther("500");

            // 模拟 Migrator 行为给 User 铸造 SKY
            // 需要先给 Admin 权限或者让 Admin 角色为 0 (ROOT)
            await sky.write.mint([user.account.address, stakeAmount], { account: admin.account });
            await sky.write.approve([lockstake.address, stakeAmount], { account: user.account });

            // 锁定
            await lockstake.write.lock([stakeAmount], { account: user.account });
            
            // 尝试立即提取应失败
            try {
                await lockstake.write.withdraw({ account: user.account });
                assert.fail("Should not withdraw during lock");
            } catch (e: any) {
                assert.ok(e.message.includes("Lock period not over"));
            }

            // 快进 31 天
            await testClient.increaseTime({ seconds: 31 * 24 * 3600 });
            await testClient.mine({ blocks: 1 });

            // 提取成功
            await lockstake.write.withdraw({ account: user.account });
            assert.equal(await sky.read.balanceOf([user.account.address]), stakeAmount);
        });
    });

    describe("4. Permit 离线授权 (ERC20Permit)", function () {
        it("USDS 应该支持签名授权存款", async function () {
            const { usds, susds, user, publicClient } = await deployFixture();
            const amount = 500n;
            const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);
            const nonce = await usds.read.nonces([user.account.address]);
            const chainId = BigInt(await publicClient.getChainId());

            // 签名
            const signature = await user.signTypedData({
                domain: { name: "Sky Dollar", version: "1", chainId, verifyingContract: usds.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: susds.address, value: amount, nonce, deadline },
            });

            const r = slice(signature, 0, 32);
            const s = slice(signature, 32, 64);
            const v = hexToNumber(slice(signature, 64, 65));

            // 执行 Permit
            await usds.write.permit([user.account.address, susds.address, amount, deadline, v, r, s]);
            assert.equal(await usds.read.allowance([user.account.address, susds.address]), amount);
        });
    });
});

3. 部署脚本

js 复制代码
// scripts/deploy.js
import { network, artifacts } from "hardhat";
import { parseUnits } from "viem";
async function main() {
  // 连接网络
  const { viem } = await network.connect({ network: network.name });//指定网络进行链接
  
  // 获取客户端
  const [deployer, investor] = await viem.getWalletClients();
  const publicClient = await viem.getPublicClient();
 
  const deployerAddress = deployer.account.address;
   console.log("部署者的地址:", deployerAddress);
  
  // 部署SoulboundIdentity合约
  const SkyAccessManagerArtifact = await artifacts.readArtifact("SkyAccessManager");
  // 1. 部署合约并获取交易哈希
  const SkyAccessManagerHash = await deployer.deployContract({
    abi: SkyAccessManagerArtifact.abi,
    bytecode: SkyAccessManagerArtifact.bytecode,
    args: [deployerAddress],
  });
  const SkyAccessManagerReceipt = await publicClient.waitForTransactionReceipt({ 
     hash: SkyAccessManagerHash 
   });
   console.log("SkyAccessManager合约地址:", SkyAccessManagerReceipt.contractAddress);
   const USDSArtifact = await artifacts.readArtifact("USDS");
    const USDSHash = await deployer.deployContract({
      abi: USDSArtifact.abi,
      bytecode: USDSArtifact.bytecode,
      args: [SkyAccessManagerReceipt.contractAddress],
    });
    const USDSReceipt = await publicClient.waitForTransactionReceipt({ 
      hash: USDSHash 
    });
    console.log("USDS合约地址:", USDSReceipt.contractAddress);
    const SKYArtifact = await artifacts.readArtifact("SKY");
    const SKYHash = await deployer.deployContract({
      abi: SKYArtifact.abi,
      bytecode: SKYArtifact.bytecode,
      args: [SkyAccessManagerReceipt.contractAddress],
    });
    const SKYReceipt = await publicClient.waitForTransactionReceipt({ 
      hash: SKYHash 
    });
    console.log("SKY合约地址:", SKYReceipt.contractAddress);
    const sUSDSArtifact = await artifacts.readArtifact("sUSDS");
    const sUSDSHash = await deployer.deployContract({   
      abi: sUSDSArtifact.abi,
      bytecode: sUSDSArtifact.bytecode,
      args: [USDSReceipt.contractAddress, SkyAccessManagerReceipt.contractAddress],
    });
    const sUSDSReceipt = await publicClient.waitForTransactionReceipt({ 
      hash: sUSDSHash 
    });
    console.log("sUSDS合约地址:", sUSDSReceipt.contractAddress);    
    const SkyLockstakeArtifact = await artifacts.readArtifact("SkyLockstake");
    const SkyLockstakeHash = await deployer.deployContract({   
      abi: SkyLockstakeArtifact.abi,
      bytecode: SkyLockstakeArtifact.bytecode,
      args: [SKYReceipt.contractAddress,SkyAccessManagerReceipt.contractAddress],
    });
    const SkyLockstakeReceipt = await publicClient.waitForTransactionReceipt({ 
      hash: SkyLockstakeHash 
    });
    console.log("SkyLockstake合约地址:", SkyLockstakeReceipt.contractAddress);          
}

main().catch(console.error);

结语

本文通过模块化架构与 Ray 精度算法,完整复刻了 Sky Protocol 的核心设计。利用 OpenZeppelin V5 的权限管理和 Viem 测试框架,实现了从双代币体系到储蓄收益的安全闭环。这套工程化方案为 DeFi 协议开发提供了可复用的实践模板。

相关推荐
OxYGC4 小时前
[Web3] 一文读懂区块链中的账本类型
web3·区块链
Joy T3 天前
【Web3】深度解析 NFT 跨链智能合约开发:原生资产与衍生包装合约架构实战
git·架构·web3·区块链·node·智能合约·hardhat
Joy T4 天前
【Web3】智能合约质量保障工程:从单元测试到 Gas 效能优化
单元测试·log4j·web3·智能合约·hardhat
Joy T4 天前
【Web3】NFT 元数据去中心化存储与智能合约集成实战
开发语言·web3·去中心化·区块链·php·智能合约·hardhat
亚历克斯神5 天前
Flutter 三方库 eip55 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、符合 Web3 标准的以太坊地址校验与防串改引擎
flutter·web3·harmonyos
阿晨5 天前
Solana SquadX off-chain 登录 /「类签名」
web3
竹林8186 天前
Web3前端开发:使用ethers.js监听智能合约事件
javascript·智能合约
财经汇报6 天前
Unloq发布SC+平台 包括智能合约解决清算难题
大数据·人工智能·智能合约
暴躁小师兄数据学院6 天前
【WEB3.0零基础转换笔记】Rust编程篇-第4讲:控制流
开发语言·笔记·rust·web3·区块链·智能合约