solidity学习之ERC4626
什么是ERC4626
ERC4626是对ERC20代币标准的扩展,用于实现收益金库的标准化,用户可以将资产质押到合约中,持有相应的shares凭证,通过凭证来享有合约后续的收益。
实现逻辑
ERC4626继承了ERC20,合约本身会发行一种shares代币,当用户存入指定的代币后,就会根据金库内的资产情况铸造响应的shares份额分发给用户,当用户想要赎回资产时,调用合约就会销毁shares份额,并将存入的代币和附加的收益返还给用户。
ERC4626提供了两种计算方式,用户可以根据存入的资产算出shares份额,也可以通过想要得到的份额计算出需要存入多少资产。
接口合约如下:
interface IERC4626 is IERC20, IERC20Metadata {
/*//////////////////////////////////////////////////////////////
事件
//////////////////////////////////////////////////////////////*/
// 存款时触发
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
);
/*//////////////////////////////////////////////////////////////
元数据
//////////////////////////////////////////////////////////////*/
/**
* @dev 返回金库的基础资产代币地址 (用于存款,取款)
* - 必须是 ERC20 代币合约地址.
* - 不能revert
*/
function asset() external view returns (address assetTokenAddress);
/*//////////////////////////////////////////////////////////////
存款/提款逻辑
//////////////////////////////////////////////////////////////*/
/**
* @dev 存款函数: 用户向金库存入 assets 单位的基础资产,然后合约铸造 shares 单位的金库额度给 receiver 地址
*
* - 必须释放 Deposit 事件.
* - 如果资产不能存入,必须revert,比如存款数额大大于上限等。
*/
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
/**
* @dev 铸造函数: 用户需要存入 assets 单位的基础资产,然后合约给 receiver 地址铸造 share 数量的金库额度
* - 必须释放 Deposit 事件.
* - 如果全部金库额度不能铸造,必须revert,比如铸造数额大大于上限等。
*/
function mint(uint256 shares, address receiver) external returns (uint256 assets);
/**
* @dev 提款函数: owner 地址销毁 share 单位的金库额度,然后合约将 assets 单位的基础资产发送给 receiver 地址
* - 释放 Withdraw 事件
* - 如果全部基础资产不能提取,将revert
*/
function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares);
/**
* @dev 赎回函数: owner 地址销毁 shares 数量的金库额度,然后合约将 assets 单位的基础资产发给 receiver 地址
* - 释放 Withdraw 事件
* - 如果金库额度不能全部销毁,则revert
*/
function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);
/*//////////////////////////////////////////////////////////////
会计逻辑
//////////////////////////////////////////////////////////////*/
/**
* @dev 返回金库中管理的基础资产代币总额
* - 要包含利息
* - 要包含费用
* - 不能revert
*/
function totalAssets() external view returns (uint256 totalManagedAssets);
/**
* @dev 返回利用一定数额基础资产可以换取的金库额度
* - 不要包含费用
* - 不包含滑点
* - 不能revert
*/
function convertToShares(uint256 assets) external view returns (uint256 shares);
/**
* @dev 返回利用一定数额金库额度可以换取的基础资产
* - 不要包含费用
* - 不包含滑点
* - 不能revert
*/
function convertToAssets(uint256 shares) external view returns (uint256 assets);
/**
* @dev 用于链上和链下用户在当前链上环境模拟存款一定数额的基础资产能够获得的金库额度
* - 返回值要接近且不大于在同一交易进行存款得到的金库额度
* - 不要考虑 maxDeposit 等限制,假设用户的存款交易会成功
* - 要考虑费用
* - 不能revert
* NOTE: 可以利用 convertToAssets 和 previewDeposit 返回值的差值来计算滑点
*/
function previewDeposit(uint256 assets) external view returns (uint256 shares);
/**
* @dev 用于链上和链下用户在当前链上环境模拟铸造 shares 数额的金库额度需要存款的基础资产数量
* - 返回值要接近且不小于在同一交易进行铸造一定数额金库额度所需的存款数量
* - 不要考虑 maxMint 等限制,假设用户的存款交易会成功
* - 要考虑费用
* - 不能revert
*/
function previewMint(uint256 shares) external view returns (uint256 assets);
/**
* @dev 用于链上和链下用户在当前链上环境模拟提款 assets 数额的基础资产需要赎回的金库份额
* - 返回值要接近且不大于在同一交易进行提款一定数额基础资产所需赎回的金库份额
* - 不要考虑 maxWithdraw 等限制,假设用户的提款交易会成功
* - 要考虑费用
* - 不能revert
*/
function previewWithdraw(uint256 assets) external view returns (uint256 shares);
/**
* @dev 用于链上和链下用户在当前链上环境模拟销毁 shares 数额的金库额度能够赎回的基础资产数量
* - 返回值要接近且不小于在同一交易进行销毁一定数额的金库额度所能赎回的基础资产数量
* - 不要考虑 maxRedeem 等限制,假设用户的赎回交易会成功
* - 要考虑费用
* - 不能revert.
*/
function previewRedeem(uint256 shares) external view returns (uint256 assets);
/*//////////////////////////////////////////////////////////////
存款/提款限额逻辑
//////////////////////////////////////////////////////////////*/
/**
* @dev 返回某个用户地址单次存款可存的最大基础资产数额。
* - 如果有存款上限,那么返回值应该是个有限值
* - 返回值不能超过 2 ** 256 - 1
* - 不能revert
*/
function maxDeposit(address receiver) external view returns (uint256 maxAssets);
/**
* @dev 返回某个用户地址单次铸造可以铸造的最大金库额度
* - 如果有铸造上限,那么返回值应该是个有限值
* - 返回值不能超过 2 ** 256 - 1
* - 不能revert
*/
function maxMint(address receiver) external view returns (uint256 maxShares);
/**
* @dev 返回某个用户地址单次取款可以提取的最大基础资产额度
* - 返回值应该是个有限值
* - 不能revert
*/
function maxWithdraw(address owner) external view returns (uint256 maxAssets);
/**
* @dev 返回某个用户地址单次赎回可以销毁的最大金库额度
* - 返回值应该是个有限值
* - 如果没有其他限制,返回值应该是 balanceOf(owner)
* - 不能revert
*/
function maxRedeem(address owner) external view returns (uint256 maxShares);
}
逻辑分为四部分:
- 元数据,asset()方法,返回的是基础资产,也就是存入资产的token地址。
- 存款提款逻辑,存款和提款各有两个方法,分别以asset和share为基准进行计算。
- 会计逻辑,用来计算asset和share互相转换的方法,既是存款提款的基础方法,也可以对外提供方便用户进行模拟计算。此处可以增加额外的逻辑,用来实现更复杂的算法。
- 限额逻辑,计算用户可存取的限额,与合约的设置以及用户的余额有关。
具体实现
function totalAssets() public view override returns (uint256 totalManagedAssets){
return _asset.balanceOf(address(this));
}
totalAssets就是简单地将当前合约下的asset返回。
function convertToShares(uint256 assets) public view override returns (uint256 shares){
uint256 supply = totalSupply();
return supply == 0 ? assets: assets * supply / totalAssets();
}
function convertToAssets(uint256 shares) public view override returns (uint256 assets){
uint256 supply = totalSupply();
return supply == 0? shares: shares * totalAssets() / supply;
}
convertToShares和convertToAssets是share和asset互相转换的方法,其中当supply为0时,也就是此时合约中还未有任何资产,也未有任何share,那么asset和share按照1:1的比例进行兑换。如果此时已经有资产,那么supply/totalAssets()就是每份asset可以兑换的share数量,反之亦然。
这里有两个思考点:
- 按这种计算方式,那么
asset和share始终都是1:1的,那么会出现不相等的情况是因为合约中是会有收益产生的(依赖于其他逻辑),此时asset增加而share不变,那么比例就发生了变化。 - 按理来说
supply=0时,share也应该为0,应该没有任何可兑换的asset,但此处兼容了这一场景,是为了防止revert的出现。
function previewDeposit(uint256 assets) public view override returns (uint256 shares){
return convertToShares(assets);
}
function previewMint(uint256 shares) public view returns (uint256 assets) {
return convertToAssets(shares);
}
function previewWithdraw(uint256 assets) public view returns (uint256 shares){
return convertToShares(assets);
}
function previewRedeem(uint256 shares) public view returns (uint256 assets) {
return convertToAssets(shares);
}
四个会计方法都简单地用了convert的调用,因为这里实现的是简版的合约,没有其他逻辑。
function maxDeposit(address receiver) external view returns (uint256 maxAssets){
return type(uint256).max;
}
function maxMint(address receiver) external view returns (uint256 maxShares){
return type(uint256).max;
}
function maxWithdraw(address owner) external view returns (uint256 maxAssets){
return convertToAssets(balanceOf(owner));
}
function maxRedeem(address owner) external view returns (uint256 maxShares){
return balanceOf(owner);
}
同样,限额逻辑也是简单地实现,并没有做过多地限制。
function deposit(uint256 assets, address receiver) external override returns (uint256 shares){
shares = previewDeposit(assets);
_asset.transferFrom(msg.sender, address(this), assets);
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
}
function mint(uint256 shares, address receiver) external returns (uint256 assets){
// 利用 previewMint() 计算需要存款的基础资产数额
assets = previewMint(shares);
// 先 transfer 后 mint,防止重入
_asset.transferFrom(msg.sender, address(this), assets);
_mint(receiver, shares);
// 释放 Deposit 事件
emit Deposit(msg.sender, receiver, assets, shares);
}
function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares){
shares = previewWithdraw(assets);
if(msg.sender != owner){
_spendAllowance(owner, msg.sender, shares);
}
_burn(owner, shares);
_asset.transfer(receiver, assets);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
}
function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets){
assets = previewRedeem(shares);
if(msg.sender != owner){
_spendAllowance(owner, msg.sender, shares);
}
_burn(owner, shares);
_asset.transfer(receiver, assets);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
}
可以看到deposit和mint,withdraw和redeem的实现都是基本一致的,只在于数量计算的区别。
有一个值得注意的地方就是deposit和mint只支持存入msg.sender自己的资产,但是withdraw和redeem是支持通过授权的形式由他人来调用的,所以当msg.sender不等于owner的时候,需要使用授权额度。
solidity学习之ERC4626的更多相关文章
- solidity学习-cryptoPunks为实例
在这里使用cryptoPunks为实例来进行solidity的介绍,一般这些内容理解了就能够进行相对简单的智能合约的编写了,同时会添加一些我认为也十分重要的内容学习文档为http://solidity ...
- solidity 学习笔记(7)内联汇编
为什么要有内联汇编? //普通循环和内敛汇编循环比较 pragma solidity ^0.4.25; contract Assembly{ function nativeLoop() public ...
- solidity 学习笔记(6)call 函数
call() 方法 call()是一个底层的接口,用来向一个合约发送消息,也就是说如果你想实现自己的消息传递,可以使用这个函数.函数支持传入任意类型的任意参数,并将参数打包成32字节,相互拼接后向合约 ...
- solidity 学习笔记(5)接口
接口:不用实现方法,仅仅定义方法. pragma solidity ^; contract cat{ //cat实际上实现了接口animalEat,因为他们有相同的方法. string name; f ...
- solidity 学习笔记(3) 函数修饰符/继承
修饰符: 函数修饰符有 constant view pure 其中 constant和view的功能是一样的 不会消耗gas 也就是说不会做任何存储 constant在5.0以后的版本中被废弃 ...
- solidity 学习笔记 2 (二维数组)
solidity 二维数组: pragma solidity ^0.4.23; contract twoArray{ uint[2][3] grade =[[20,30],[40,50],[45,60 ...
- solidity学习笔记
一 pragam solidity ^0.4.23; contract helloword{ string public name ="hello"; function getN ...
- 以太坊智能合约开发 Solidity学习
1. pragma solidity >=0.4.22 <0.6.0;//版本号,头文件 contract BooleanTest { bool _a;//默认返回false int nu ...
- solidity 学习笔记(4)library库
library库的申明: library SafeMath{ functrion mul(uint a,uint b) public returns (uint){ uint c= a*b; asse ...
- cryptopunks的代码解释
1.imageHash就是将punk所有图像合在一起的那张图punks.png进行hash得到一个值,并将该值存储到链上,用处就是你可以通过将图像hash然后跟该值对比看图像对不对.这就是它的用处,在 ...
随机推荐
- k8s集群根据进程PID获取Pod名称
简单说明 在实际的应用场景中,我们如果看到某个进程资源或服务异常,需要根据这个进程排查到底是哪个服务的Pod,这里我们介绍一种根据PID快速寻找Pod名称的方法. 实际操作 查看进程PID 这里我们以 ...
- Golang解析yaml文件
一.具体思路 将配置yaml文件内容解析为我们定义好的struct,这种比较简单,如果想获取对应的值,直接获取即可. 二.实现步骤 首先根据配置文件的内容定义一个结构体Config,结构体类型和yam ...
- ChatClient vs ChatModel:开发者必须知道的4大区别!
在 Spring AI/Spring AI Alibaba 框架中,ChatModel 和 ChatClient 都可以实现大模型的文本生成功能,例如聊天机器人,但二者是两种不同层级的 API 封装, ...
- C#.Net筑基-优雅LINQ的查询艺术
Linq(Language Integrated Query,集成查询语言),顾名思义就是用来查询数据的一种语言(可以看作是一组功能.框架特性的集合).在.NETFramework3.5(大概2007 ...
- 六、Linux系统 DRM调试工具modetest
4.20.modetest(调试 DRM) modetest 是 libdrm 提供的一个测试工具,用于调试 DRM(Direct Rendering Manager) 设备,它能够列出可用的显示设备 ...
- 英文单词字母大小写在线转换工具html代码
这是一个简单而实用的在线大小写转换工具.它允许用户输入任意文本,并提供三种转换选项:转换为全大写.全小写或首字母大写. 使用这个工具非常简单快捷.用户只需要在输入框中输入想要转换的文本,选择合适的转换 ...
- jq通过自定义属性获取标签对象
https://blog.csdn.net/qq_44927883/article/details/106360358$("td[data-field='fieldHtmlType']&qu ...
- Oracle中数值型及处理方法
数值型 理解精度 number类型的精度表示可以标识数据精确度的位数.对于数字13245.977,当精确到小数点后2位,数据为12345.98,此时精度为7.而当精确到小数点前2位,数据为12300, ...
- type和interface与namespace和module
type 和 interface 的区别 type 可以定义基本类型 也可以定义对象.数组.函数等 // 1. 定义基本类型 type StudentNameType = string; const ...
- 利用alist把百度网盘变成对象存储
官网 其实官方教程就挺全的:https://alist.nn.ci/zh/ 一些三方教程 https://www.bilibili.com/read/cv19106579/ https://zhuan ...