ERC4626代币金库(Vault)是ERC20的拓展。本质上可以理解为一个智能合约,支持把资产托管进去这个合约中,合约代表你去赚钱,赚到的钱按份额分配。ERC4626继承于ERC20,基本接口和ERC20一致,但添加了增强的存款、取款、赎回、会计等接口:https://learnblockchai
ERC4626
代币金库(Vault
)是 ERC20
的拓展。本质上可以理解为一个智能合约,支持把资产托管进去这个合约中,合约代表你去赚钱,赚到的钱按份额分配。
ERC4626
继承于 ERC20
,基本接口和 ERC20
一致,但添加了增强的存款、取款、赎回、会计等接口。
ERC4626
是建立在 ERC20
之上的扩展标准,用于构建「资产托管 + 份额映射 + 收益分配」的智能金库系统。简单来说,ERC20
是货币,是金钱。而 ERC4626
是基于金钱而建立的基金,相当于把钱托管到基金里面让它帮我们操作,赚到的钱按份额平分。
代币金库:单资产。用于借贷转利润。通常无锁仓机制。 可以将代币金库类比于基金,把资产交给他就能给你赚取收益,通过份额来赎回。
质押:单资产。用于协议奖励、出块奖励。有锁仓机制。
质押一般用于协议的运行(PoS
),需要锁仓、解锁。收益一般来自于治理分红、协议通胀奖励。
流动性池:多资产。用于做市赚取手续费。无锁仓机制。
流动性提供者可以类比与交易所的做市商角色。通过将资金给到流动性池中,赚取交易的手续费(如 0.3%
),但有可能需要承担价格变动的亏损。
下面我们来分析 ERC4626
中的核心状态变量。
abstract contract ERC4626 is ERC20, IERC4626 {
using Math for uint256;
IERC20 private immutable _asset;
uint8 private immutable _underlyingDecimals;
...
}
在上面代码 ERC4626
的代码片段中,可以看出,ERC4626
是继承自 ERC20
的,故 ERC4626
拥有其所有状态变量和函数。有关 ERC20 请看:https://learnblockchain.cn/article/14429
IERC20 private immutable _asset
:
这是一个指向 IERC20
的接口,存储着外部资产的合约地址。例如我们有一个外部资产 USDT
,和我们金库内嵌的 vUSDT
进行交互。此处存储的就是 USDT
的合约地址。
uint8 private immutable _underlyingDecimals
:
这是一个存储外部资产的精度变量。例如我们有外部资产 USDT
,则会在此处存储 6
。(因为我们金库内嵌的资产的精度可能与之不同,例如我们设置 vUSDT
的精度为 10
,此处存储可以为我们提供一个 USDT
的精度缓存,方便我们合约进行精度换算)
注意:为避免资产换算问题,内嵌 ERC20
和外部资产的精度在真实场景下一般是设置为相同的(绝大多数项目都是这样设计的)。如果精度设置不一致,需要在 deposit()、mint()、withdraw()、redeem()
等函数中手动进行份额与资产的换算。
ERC4626
的接口相对而言较多,我们分为核心、会计、辅助接口三个板块进行讲解。
/**
返回金库内嵌的 ERC20 合约地址
*/
function asset() external view returns (address assetTokenAddress);
/**
存款,用于存基础资产,获取相应份额给 receiver
*/
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
/**
铸造,和存款类似。拥有铸造相应的份额给 receiver (会换算基础资产)
*/
function mint(uint256 shares, address receiver) external returns (uint256 assets);
/**
提现,提现基础资产给 receiver,相应的 owner 需要销毁相应份额
*/
function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares);
/**
赎回,和提现类似。赎回相应份额给 receiver,owner 销毁相应资产(会换算成基础资产)
*/
function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);
其中存款、提现可以看做成用外部资产(如 USDT
当单位进行操作)
而铸造、赎回可以看做用内嵌资产(如 vUSDT
当单位进行操作)
totalAssets()
: 返回金库中管理的资产总额。covertToShares()
: 返回基础资产可换取的金库额度。previewDeposit()
: 模拟存款一定基础资产可以换取的金库份额。previewMint()
: 模拟铸造一定金库份额需要存基础资产的数量。previewWithdraw()
: 模拟提现一定数量的基础资产需要消耗的份额。previewRedeem()
: 模拟销毁一定金库份额能赎回的基础资产数量。maxDeposit()
: 返回最大单次可存的资产。maxMint()
: 返回单次可铸造最大金库份额。maxWithdraw()
: 返回单次最大可提现资产。maxRedeem()
: 返回单次最大赎回份额。ERC4626
的事件较为简单,只有两个。分别是存款、取款。
// 存款、铸造时触发
event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);
// 提现、赎回时触发
event Withdraw(
address indexed sender,
address indexed receiver,
address indexed owner,
uint256 assets,
uint256 shares
);
ERC4626
的金库 Shawn4626.sol
通过继承 ERC4626
来实现一个简单的代币金库合约contract Shawn4626 is ERC4626 {
uint256 constant MAX_SUPPLY = 100 * 10 ** 18; // 最小单位,供应 100 个 vUSDT
// 为避免精度换算问题,统一用 ERC 内置的精度 18
// uint8 constant VUSDT_DECIMAL = 10;
// 初始化出来内嵌的 ERC20 代币和外部资产的地址
constructor(IERC20 asset_) ERC20("Voult USDT","vUSDT") ERC4626(asset_) {
}
/// 限制最大 mint 出的 vUSDT 数量为 100(10^18 最小单位)
function deposit(uint256 assets, address receiver) public override returns (uint256) {
uint256 shares = previewDeposit(assets);
require(totalSupply() + shares <= MAX_SUPPLY, "Exceeds max supply");
_deposit(msg.sender, receiver, assets, shares);
return shares;
}
/// 如果允许直接 mint,可以在这里限制 max supply
function mint(uint256 shares, address receiver) public override returns (uint256 assets) {
require(totalSupply() + shares <= MAX_SUPPLY, "Exceeds max supply");
super.mint(shares, receiver);
return shares;
}
}
TestErc20.sol
因为 ERC4626
代币金库合约创建时需要传入外部资产的地址,需要依赖到 ERC20
的合约,所以我们这里简单实现一个 ERC20
的合约作为代币金库的外部资产依赖。contract TestERC20 is ERC20 {
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {
}
// 铸币(当前任何人都能铸币,实际应加上权限控制)
function mint(address user,uint amount) external {
_mint(user, amount);
}
}
Shaw4626.t.sol
在这个测试中,分别测试 deposit()、mint()
的正常存款流程以及超过最大额度存款的流程、混合存款的流程。contract TestShawn4626 is Test {
Shawn4626 vault;
TestERC20 asset;
uint256 constant MAX_SUPPLY = 100 * 10 ** 18; // 最大供应量 100 个 vUSDT(考虑精度)
uint256 constant DEPOSIT_AMOUNT = 1 * 10 ** 18; // 每次存入的资产量(考虑精度)
function setUp() public {
// 创建一个 ERC20 资产合约,作为外部资产
asset = new TestERC20("Test USDT", "USDT");
// 部署 Shawn4626 Vault 合约
vault = new Shawn4626(IERC20(address(asset)));
// 给用户提供一些资产
asset.mint(address(this),DEPOSIT_AMOUNT * 5); // 给用户 5 倍的存款量
}
// 测试最大供应量限制:存入资产时超过最大供应量应该失败
function testDepositExceedsMaxSupply() public {
// 存入资产,超过最大供应量
asset.approve(address(vault), DEPOSIT_AMOUNT);
vm.expectRevert("Exceeds max supply");
vault.deposit(DEPOSIT_AMOUNT * 101, address(this));
}
// 测试最大供应量限制:mint 超过最大供应量时应该失败
function testMintExceedsMaxSupply() public {
asset.approve(address(vault), DEPOSIT_AMOUNT * 101);
vm.expectRevert("Exceeds max supply");
// 直接 mint 超过最大供应量
vault.mint(DEPOSIT_AMOUNT * 101, address(this));
}
// 测试正常的存款操作
function testNormalDeposit() public {
asset.approve(address(vault), DEPOSIT_AMOUNT);
vault.deposit(DEPOSIT_AMOUNT, address(this)); // 正常存款
assertEq(vault.totalSupply(), DEPOSIT_AMOUNT); // 确保总供应量增加
}
// 测试正常的 mint 操作
function testNormalMint() public {
asset.approve(address(vault), type(uint256).max);
vault.mint(DEPOSIT_AMOUNT, address(this)); // 正常 mint
assertEq(vault.totalSupply(), DEPOSIT_AMOUNT); // 确保总供应量增加
}
// 测试正常的存款和 mint 组合
function testDepositAndMint() public {
asset.approve(address(vault), type(uint256).max);
// 默认 1:1 兑换
console2.log("share of DEPOSIT_AMOUNT is " , vault.previewDeposit(DEPOSIT_AMOUNT));
vault.deposit(DEPOSIT_AMOUNT, address(this)); // 正常存款
vault.mint(DEPOSIT_AMOUNT, address(this)); // 再正常 mint
assertEq(vault.totalSupply(), DEPOSIT_AMOUNT * 2); // 确保总供应量正确
}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!