前言
随着 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 协议开发提供了可复用的实践模板。