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

  • 木西
  • 发布于 12小时前
  • 阅读 30

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

前言

随着 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 标准实现了代币化仓位。

核心公式

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

$$TotalAssets = UnderlyingAssets \times \frac{RAY + (SSR - RAY) \times \Delta t}{365 days \times RAY}$$

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


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

1. 核心智能合约

  • 访问管理器(SkyAccessManager)
    
    // 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)
    
    // 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)

    // 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 应该支持签名授权存款。
    
    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 协议开发提供了可复用的实践模板。

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

0 条评论

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