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的時候,需要使用授權額度。

浙公網安備 33010602011771號