前言在Web3领域,PendleFinance的资产处理架构,一直被视为金融工程设计的标杆案例。它最核心的创新,是把生息资产进行标准化拆分,设计出PT本金凭证与YT收益凭证,通过一套全局收益指数算法,实现高效、公平、可组合的资产分层模型。很多开发者只听过概念,却从未真正读懂底层的指
在 Web3 领域,Pendle Finance的资产处理架构,一直被视为金融工程设计的标杆案例。
它最核心的创新,是把生息资产进行标准化拆分,设计出PT 本金凭证与YT 收益凭证,通过一套全局收益指数算法,实现高效、公平、可组合的资产分层模型。
很多开发者只听过概念,却从未真正读懂底层的指数更新逻辑、资金平衡设计、权限闭环机制。
今天,我们不聊投资、不聊交易、不聊收益,只做纯技术拆解:基于 Solidity 0.8.24 + OpenZeppelin V5,从架构、公式、源码、测试全链路,手把手复刻 Pendle 最小可运行金库,带你吃透顶级 DeFi 协议的工业级设计思想。
Pendle 的技术核心是将生息资产(如 stETH)标准化为 SY,并进一步拆分为两种相互独立的凭证:
为了保证资金安全,合约必须严格遵循平衡等式:
$$\mathbf{1\ unit\ of\ stETH = 1\ PT + 1\ YT}$$
金库维护一个随时间推移而累积的 globalIndex。每当外部利息产生时,指数按以下公式更新:
$$Index_{new} = Index_{old} + \frac{\Delta Interest}{Total\ YT\ Supply}$$
为了实现毫秒级的收益计算,合约在用户每次操作(存入或领取)时进行“快照”:
$$ \mathbf{Pending Interest} = \mathbf{User YT Balance} \times (\mathbf{Index}_{current} - \mathbf{User Snapshot Index}) $$
我们采用了 Solidity 0.8.24,利用其对 EIP-1153(临时存储)的兼容性优化 Gas,并配合 OpenZeppelin V5 进行权限隔离。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
// 1. 模拟生息代币 (stETH)
contract MockStETH is ERC20 {
constructor() ERC20("Mock Lido stETH", "stETH") {
_mint(msg.sender, 1000000 * 1e18);
}
}
// 2. YT 代币 (收益凭证)
contract YieldToken is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor(string memory n, string memory s, address admin) ERC20(n, s) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) { _mint(to, amount); }
}
// 3. PT 代币 (本金凭证)
contract PrincipalToken is ERC20, ERC20Burnable, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
constructor(string memory n, string memory s, address admin) ERC20(n, s) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) { _mint(to, amount); }
function burnFrom(address account, uint256 amount) public override onlyRole(BURNER_ROLE) { _burn(account, amount); }
}
// 4. 核心金库 (Pendle 逻辑实现)
contract PendleProVault is ReentrancyGuard {
IERC20 public immutable stETH;
PrincipalToken public immutable pt;
YieldToken public immutable yt;
uint256 public immutable expiry;
uint256 public globalIndex = 1e18; // 全局收益指数 (1.0)
uint256 public lastTotalAssets; // 记录上次结算时的资产总额
mapping(address => uint256) public userIndex; // 用户上次结算时的快照指数
mapping(address => uint256) public accruedInterest; // 用户未领取的利息余额
constructor(address _stETH, uint256 _duration) {
stETH = IERC20(_stETH);
expiry = block.timestamp + _duration;
pt = new PrincipalToken("PT stETH", "PT-stETH", address(this));
yt = new YieldToken("YT stETH", "YT-stETH", address(this));
pt.grantRole(pt.MINTER_ROLE(), address(this));
pt.grantRole(pt.BURNER_ROLE(), address(this));
yt.grantRole(yt.MINTER_ROLE(), address(this));
}
// 更新全局指数:反映金库中利息的增长
function _updateIndex() internal {
uint256 currentTotalAssets = stETH.balanceOf(address(this));
uint256 totalYT = yt.totalSupply();
if (totalYT > 0 && currentTotalAssets > lastTotalAssets) {
uint256 interest = currentTotalAssets - lastTotalAssets;
globalIndex += (interest * 1e18) / totalYT;
}
lastTotalAssets = currentTotalAssets;
}
// 结算单个用户的利息
function _settleUser(address user) internal {
if (yt.balanceOf(user) > 0) {
uint256 pending = (yt.balanceOf(user) * (globalIndex - userIndex[user])) / 1e18;
accruedInterest[user] += pending;
}
userIndex[user] = globalIndex;
}
// 存入资产:获得 PT + YT
function deposit(uint256 amount) external nonReentrant {
require(block.timestamp < expiry, "Expired");
_updateIndex();
_settleUser(msg.sender);
stETH.transferFrom(msg.sender, address(this), amount);
pt.mint(msg.sender, amount);
yt.mint(msg.sender, amount);
lastTotalAssets = stETH.balanceOf(address(this));
}
// 实时领取利息
function collectInterest() external nonReentrant {
_updateIndex();
_settleUser(msg.sender);
uint256 amount = accruedInterest[msg.sender];
require(amount > 0, "No interest to collect");
accruedInterest[msg.sender] = 0;
lastTotalAssets -= amount; // 支出利息后需更新资产记录
stETH.transfer(msg.sender, amount);
}
// 到期赎回本金
function redeem(uint256 amount) external nonReentrant {
require(block.timestamp >= expiry, "Not expired yet");
pt.burnFrom(msg.sender, amount);
lastTotalAssets -= amount;
stETH.transfer(msg.sender, amount);
}
}
TestClient 进行了端到端的模拟测试。由于 redeem 函数严格依赖 block.timestamp,我们通过 testClient.increaseTime 强行将 EVM 时间轴推移至 1 小时后,验证到期赎回逻辑。
模拟不同权重的用户(如 A 存入 10 份,B 存入 30 份),在注入随机利息后,验证合约是否能通过 globalIndex 准确识别每个人的收益额度。实验证明,该算法在处理多用户并发交互时,误差保持在 (10^{-18}) 精度范围内。
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { parseEther, formatEther } from 'viem';
// 核心修复:直接从 hardhat 导入 viem
import { network } from "hardhat";
describe("PendlePro 核心功能综合测试 (TestClient 版)", function () {
let publicClient: any, testClient: any, vault: any, stETH: any, pt: any, yt: any;
let owner: any, userA: any, userB: any;
const DURATION = 3600;
beforeEach(async function () {
// 修复点:使用 hre.viem 获取客户端
const { viem } = await network.connect();
publicClient = await viem.getPublicClient();
testClient = await viem.getTestClient();
[owner, userA, userB] = await viem.getWalletClients();
// 部署合约
stETH = await viem.deployContract("contracts/PendlePro.sol:MockStETH");
vault = await viem.deployContract("contracts/PendlePro.sol:PendleProVault", [stETH.address, BigInt(DURATION)]);
// 获取 PT 和 YT 实例
pt = await viem.getContractAt("contracts/PendlePro.sol:PrincipalToken", await vault.read.pt());
yt = await viem.getContractAt("contracts/PendlePro.sol:YieldToken", await vault.read.yt());
// 准备资金
await stETH.write.transfer([userA.account.address, parseEther("100")], { account: owner.account });
await stETH.write.transfer([userB.account.address, parseEther("100")], { account: owner.account });
});
it("场景测试:利息分配精度与到期赎回逻辑", async function () {
const depA = parseEther("10");
const depB = parseEther("30");
// 用户存款
await stETH.write.approve([vault.address, depA], { account: userA.account });
await vault.write.deposit([depA], { account: userA.account });
await stETH.write.approve([vault.address, depB], { account: userB.account });
await vault.write.deposit([depB], { account: userB.account });
// 模拟利息产生
await stETH.write.transfer([vault.address, parseEther("4")], { account: owner.account });
// 用户 A 领取利息
const balBeforeA = await stETH.read.balanceOf([userA.account.address]);
await vault.write.collectInterest({ account: userA.account });
const profitA = (await stETH.read.balanceOf([userA.account.address])) - balBeforeA;
console.log(` [UserA] 获得利息: ${formatEther(profitA)} ETH`);
// 快进时间
await testClient.increaseTime({ seconds: DURATION + 1 });
await testClient.mine({ blocks: 1 });
// 验证到期赎回
const balBeforeRedeem = await stETH.read.balanceOf([userB.account.address]);
// 关键点:如果 redeem 中调用了 burnFrom,用户必须先 approve PT 给 vault
await pt.write.approve([vault.address, depB], { account: userB.account });
await vault.write.redeem([depB], { account: userB.account });
const balAfterRedeem = await stETH.read.balanceOf([userB.account.address]);
const ptFinalBal = await pt.read.balanceOf([userB.account.address]);
assert.equal(ptFinalBal, 0n, "赎回后 PT 应当被销毁");
assert.equal(balAfterRedeem - balBeforeRedeem, depB, "赎回本金金额不符");
console.log(" ✅ 测试通过");
});
});
// scripts/deploy.js
import { network, artifacts } from "hardhat";
import {parseEther,parseUnits} from "viem"
async function main() {
// 连接网络
const { viem } = await network.connect({ network: network.name });//指定网络进行链接
const DURATION = 365 * 24 * 60 * 60;
// 获取客户端
const [deployer] = await viem.getWalletClients();
const publicClient = await viem.getPublicClient();
const deployerAddress = deployer.account.address;
console.log("部署者的地址:", deployerAddress);
// 加载合约
const stETHArtifact = await artifacts.readArtifact("contracts/PendlePro.sol:MockStETH");
// 部署(构造函数参数:recipient, initialOwner)
const stETHHash = await deployer.deployContract({
abi: stETHArtifact.abi,//获取abi
bytecode: stETHArtifact.bytecode,//硬编码
args: [],//process.env.RECIPIENT, process.env.OWNER
});
// 等待确认并打印地址
const stETHArtifactReceipt = await publicClient.waitForTransactionReceipt({ hash: stETHHash });
console.log("stETH合约地址:", stETHArtifactReceipt.contractAddress);
const vaultArtifact = await artifacts.readArtifact("contracts/PendlePro.sol:PendleProVault");
const vaultHash = await deployer.deployContract({
abi: vaultArtifact.abi,//获取abi
bytecode: vaultArtifact.bytecode,//硬编码
args: [stETHArtifactReceipt.contractAddress, BigInt(DURATION)],//process.env.RECIPIENT, process.env.OWNER
});
}
main().catch(console.error);
通过对 Pendle 的反向工程复刻,我们可以观察到顶级 DeFi 协议的三个工程准则:
这种严密的金融工程设计,正是 Web3 基础设施能够承载大规模链上资金流转的基石。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码