diff --git a/standards/Forc.toml b/standards/Forc.toml index 0ca2b5e..70b44c3 100644 --- a/standards/Forc.toml +++ b/standards/Forc.toml @@ -1,2 +1,2 @@ [workspace] -members = ["src_3", "src_5", "src_20"] +members = ["src_3", "src_5", "src_6", "src_20"] diff --git a/standards/src_6/README.md b/standards/src_6/README.md index 1d4e105..c57eeb8 100644 --- a/standards/src_6/README.md +++ b/standards/src_6/README.md @@ -22,9 +22,9 @@ Token vaults have been thoroughly explored on Ethereum and with [EIP 4626](https ## Required public functions The following functions MUST be implemented (on top of the SRC-20 functions) to follow the SRC-6 standard -### `fn deposit(receiver: Identity) -> u64` +### `fn deposit(receiver: Identity, sub_id: SubId) -> u64` Method that allows depositing of the underlying asset in exchange for shares of the vault. -This function takes the receiver's identity as an argument and returns the amount of shares minted to the receiver. +This function takes the receiver's identity and the sub_id of the sub vault as an argument and returns the amount of shares minted to the receiver. MUST revert if any AssetId other than the underlying is forwarded. MUST mint `preview_deposit(deposited_assets)` amount of shares to `receiver`. @@ -33,9 +33,9 @@ MUST increase `total_supply` of the share's AssetId by newly minted shares. MUST increase `total_assets` by one if the the AssetId is minted for the first time. MUST emit a `Deposit` log. -### `fn withdraw(asset: AssetId, receiver: Identity) -> u64` +### `fn withdraw(asset: AssetId, sub_id: SubId, receiver: Identity) -> u64` Method that allows the redeeming of the vault shares in exchange for a pro-rata amount of the underlying asset -This function takes the asset's AssetId and the receiver's identity as arguments and returns the amount of assets transferred to the receiver. +This function takes the asset's AssetId, the sub_id of the sub vault, and the receiver's identity as arguments and returns the amount of assets transferred to the receiver. The AssetId of the asset, and the AssetId of the shares MUST be one-to-one, meaning every deposited AssetId shall have a unique corresponding shares AssetId. MUST revert if any AssetId other than the AssetId corresponding to the deposited asset is forwarded. @@ -45,42 +45,58 @@ MUST reduce `managed_assets` by `preview_withdraw(redeemed_shares)`. MUST reduce `total_supply` of the shares's AssetId by amount of burnt shares. MUST emit a `Withdraw` log. -### `fn managed_assets(asset: AssetId) -> u64` +### `fn managed_assets(asset: AssetId, sub_id: SubId) -> u64` Method that returns the total assets under management by vault. Includes assets controlled by the vault but not directly possessed by vault. -This function takes the asset's AssetId as an argument and returns the total amount of assets of AssetId under management by vault. +This function takes the asset's AssetId and the sub_id of the sub vault as an argument and returns the total amount of assets of AssetId under management by vault. MUST return total amount of assets of underlying AssetId under management by vault. MUST return 0 if there are no assets of underlying AssetId under management by vault. MUST NOT revert under any circumstances. -### `fn convert_to_shares(asset: AssetId, assets: u64) -> Option` +### `fn convert_to_shares(asset: AssetId, sub_id: SubId, assets: u64) -> Option` Helper method for converting assets to shares. -This function takes the asset's AssetId and the amount of assets as arguments and returns the amount of shares that would be minted for the given amount of assets, in an ideal condition without slippage. +This function takes the asset's AssetId, the sub_id of the sub vault, and the amount of assets as arguments and returns the amount of shares that would be minted for the given amount of assets, in an ideal condition without slippage. MUST return an Option::Some of the amount of shares that would be minted for the given amount of assets, without accounting for any slippage, if the given asset is supported. MUST return an Option::None if the given asset is not supported. MUST NOT revert under any circumstances. -### `fn convert_to_assets(asset: AssetId, shares: u64) -> Option` +### `fn convert_to_assets(asset: AssetId, sub_id: SubId, shares: u64) -> Option` Helper method for converting shares to assets. -This function takes the asset's AssetId and the amount of shares as arguments and returns the amount of assets that would be transferred for the given amount of shares, in an ideal condition without slippage. +This function takes the asset's AssetId, the sub_id of the sub vault, and the amount of shares as arguments and returns the amount of assets that would be transferred for the given amount of shares, in an ideal condition without slippage. MUST return an Option::Some of the amount of assets that would be transferred for the given amount of shares, if the given asset is supported. MUST return an Option::None if the asset is not supported. MUST NOT revert under any circumstances. -### `fn max_depositable(asset: AssetId) -> Option` +### `fn max_depositable(asset: AssetId, sub_id: SubId) -> Option` Helper method for getting maximum depositable -This function takes the asset's AssetId as an argument and returns the maximum amount of assets that can be deposited into the contract, for the given asset. +This function takes the asset's AssetId and the sub_id of the sub vault as an argument and returns the maximum amount of assets that can be deposited into the contract, for the given asset. MUST return the maximum amount of assets that can be deposited into the contract, for the given asset. -### `fn max_withdrawable(asset: AssetId) -> Option` +### `fn max_withdrawable(asset: AssetId, sub_id: SubId) -> Option` Helper method for getting maximum withdrawable -This function takes the asset's AssetId as an argument and returns the maximum amount of assets that can be withdrawn from the contract, for the given asset. +This function takes the asset's AssetId and the sub_id of the sub vault as an argument and returns the maximum amount of assets that can be withdrawn from the contract, for the given asset. MUST return the maximum amount of assets that can be withdrawn from the contract, for the given asset. +### `fn vault_asset_id(asset: AssetId, sub_id: SubId) -> Option` +Method that returns the AssetId of the vault shares for the given asset and sub vault. +This function takes the asset's AssetId and the SubId of the vault as arguments and returns the AssetId of the vault shares for the given asset and sub vault. + +MUST return an Option::Some of the AssetId of the vault shares for the given asset and sub vault, if the given asset is supported. +MUST return an Option::None if the given asset is not supported. +MUST NOT revert under any circumstances. + +### `fn asset_of_vault(vault_asset_id: AssetId) -> Option` +Method that returns the AssetId of the asset of the vault for the given AssetId of the vault shares. +This function takes the AssetId of the vault shares as an argument and returns the AssetId of the asset of the vault for the given AssetId of the vault shares. + +MUST return an Option::Some of the AssetId of the asset of the vault for the given AssetId of the vault shares, if the given asset is supported and the vault has been initialised. +MUST return an Option::None if the given asset is not supported or the vault has not been initialised. +MUST NOT revert under any circumstances. + ## Required logs The following logs MUST be emitted at the specified occasions @@ -93,13 +109,15 @@ pub struct Deposit { receiver: Identity, /// The asset being deposited. asset: AssetId, + /// The SubId of the vault. + sub_id: SubId, /// The amount of assets being deposited. assets: u64, /// The amount of shares being minted. shares: u64, } ``` -`caller` has called the `deposit` method sending `assets` assets of the `asset` AssetId, in exchange for `shares` shares sent to the receiver `receiver` +`caller` has called the `deposit` method sending `assets` assets of the `asset` AssetId to the subvault of `sub_id`, in exchange for `shares` shares sent to the receiver `receiver` The `Deposit` struct MUST be logged whenever new shares are minted via the `deposit` method @@ -112,13 +130,15 @@ pub struct Withdraw { receiver: Identity, /// The asset being withdrawn. asset: AssetId, + /// The SubId of the vault. + sub_id: SubId, /// The amount of assets being withdrawn. assets: u64, /// The amount of shares being burned. shares: u64, } ``` -`caller` has called the `withdraw` method sending `shares` shares in exchange for `assets` assets of the `asset` AssetId to the receiver `receiver` +`caller` has called the `withdraw` method sending `shares` shares in exchange for `assets` assets of the `asset` AssetId from the subvault of `sub_id` to the receiver `receiver` The `Withdraw` struct MUST be logged whenever shares are redeemed for assets via the `withdraw` method diff --git a/standards/src_6/examples/simple_vault.sw b/standards/src_6/examples/simple_vault.sw new file mode 100644 index 0000000..872cc4d --- /dev/null +++ b/standards/src_6/examples/simple_vault.sw @@ -0,0 +1,185 @@ +contract; + +use std::{ + auth::msg_sender, + call_frames::msg_asset_id, + context::msg_amount, + hash::Hash, + storage::{ + storage_map::*, + storage_string::StorageString, + }, + token::{ + burn, + mint, + transfer, + }, +}; + +use src_6::{Deposit, SRC6, Withdraw}; +use src_20::SRC20; +use std::string::String; + +storage { + total_assets: u64 = 0, + total_supply: StorageMap = StorageMap {}, + name: StorageMap = StorageMap {}, + symbol: StorageMap = StorageMap {}, + decimals: StorageMap = StorageMap {}, +} + +impl SRC6 for Contract { + #[storage(read)] + fn managed_assets(asset: AssetId) -> u64 { + managed_assets(asset) // In this implementation managed_assets and max_withdrawable are the same. However in case of lending out of assets, managed_assets should be greater than max_withdrawable. + } + + #[storage(read, write)] + fn deposit(receiver: Identity) -> u64 { + let assets = msg_amount(); + let asset = msg_asset_id(); + let shares = preview_deposit(asset, assets); + require(assets != 0, "ZERO_ASSETS"); + + let _ = _mint(receiver, asset.into(), shares); // Using the asset_id as the sub_id for shares. + storage.total_supply.insert(asset, storage.total_supply.get(asset).read() + shares); + after_deposit(); + + log(Deposit { + caller: msg_sender().unwrap(), + receiver: receiver, + asset: asset, + assets: assets, + shares: shares, + }); + + shares + } + + #[storage(read, write)] + fn withdraw(asset: AssetId, receiver: Identity) -> u64 { + let shares = msg_amount(); + require(shares != 0, "ZERO_SHARES"); + require(msg_asset_id() == AssetId::new(ContractId::this(), asset.into()), "INVALID_ASSET_ID"); + let assets = preview_withdraw(asset, shares); + + _burn(asset.into(), shares); + storage.total_supply.insert(asset, storage.total_supply.get(asset).read() - shares); + after_withdraw(); + + transfer(receiver, asset, assets); + + log(Withdraw { + caller: msg_sender().unwrap(), + receiver: receiver, + asset: asset, + assets: assets, + shares: shares, + }); + + assets + } + + #[storage(read)] + fn convert_to_shares(asset: AssetId, assets: u64) -> Option { + Option::Some(preview_deposit(asset, assets)) + } + + #[storage(read)] + fn convert_to_assets(asset: AssetId, shares: u64) -> Option { + Option::Some(preview_withdraw(asset, shares)) + } + + #[storage(read)] + fn max_depositable(asset: AssetId) -> Option { + Option::Some(18_446_744_073_709_551_615 - managed_assets(asset)) // This is the max value of u64 minus the current managed_assets. Ensures that the sum will always be lower than u64::MAX. + } + + #[storage(read)] + fn max_withdrawable(asset: AssetId) -> Option { + Option::Some(managed_assets(asset)) // In this implementation total_assets and max_withdrawable are the same. However in case of lending out of assets, total_assets should be greater than max_withdrawable. + } +} + +impl SRC20 for Contract { + #[storage(read)] + fn total_assets() -> u64 { + storage.total_assets.try_read().unwrap_or(0) + } + + #[storage(read)] + fn total_supply(asset: AssetId) -> Option { + storage.total_supply.get(asset).try_read() + } + + #[storage(read)] + fn name(asset: AssetId) -> Option { + storage.name.get(asset).read_slice() + } + + #[storage(read)] + fn symbol(asset: AssetId) -> Option { + storage.symbol.get(asset).read_slice() + } + + #[storage(read)] + fn decimals(asset: AssetId) -> Option { + storage.decimals.get(asset).try_read() + } +} + +fn managed_assets(asset: AssetId) -> u64 { + std::context::this_balance(asset) +} + +#[storage(read)] +fn preview_deposit(asset: AssetId, assets: u64) -> u64 { + let shares_supply = storage.total_supply.get(AssetId::new(ContractId::this(), asset.into())).read(); + if shares_supply == 0 { + assets + } else { + assets * shares_supply / managed_assets(asset) + } +} + +#[storage(read)] +fn preview_withdraw(asset: AssetId, shares: u64) -> u64 { + let supply = storage.total_supply.get(AssetId::new(ContractId::this(), asset.into())).read(); + if supply == shares { + managed_assets(asset) + } else { + shares * (managed_assets(asset) / supply) + } +} + +fn after_deposit() { + // Does nothing, only for demonstration purposes. +} + +fn after_withdraw() { + // Does nothing, only for demonstration purposes. +} + +#[storage(read, write)] +pub fn _mint(recipient: Identity, sub_id: SubId, amount: u64) -> AssetId { + let asset_id = AssetId::new(contract_id(), sub_id); + let supply = storage.total_supply.get(asset).try_read(); + // Only increment the number of assets minted by this contract if it hasn't been minted before. + if supply.is_none() { + storage.total_assets.write(_total_assets(storage.total_assets) + 1); + } + let current_supply = supply.unwrap_or(0); + storage.total_supply.insert(asset_id, current_supply + amount); + mint_to(recipient, sub_id, amount); + asset_id +} + +#[storage(read, write)] +pub fn _burn(sub_id: SubId, amount: u64) { + let asset_id = AssetId::new(contract_id(), sub_id); + require(this_balance(asset_id) >= amount, BurnError::NotEnoughTokens); + // If we pass the check above, we can assume it is safe to unwrap. + let supply = storage.total_supply.get(asset).try_read().unwrap(); + storage.total_supply.insert(asset_id, supply - amount); + burn(sub_id, amount); +} diff --git a/standards/src_6/src/src_6.sw b/standards/src_6/src/src_6.sw index 85dea0f..5f4cc9c 100644 --- a/standards/src_6/src/src_6.sw +++ b/standards/src_6/src/src_6.sw @@ -8,6 +8,8 @@ pub struct Deposit { receiver: Identity, /// The asset being deposited. asset: AssetId, + /// The SubId of the vault. + sub_id: SubId, /// The amount of assets being deposited. assets: u64, /// The amount of shares being minted. @@ -22,6 +24,8 @@ pub struct Withdraw { receiver: Identity, /// The asset being withdrawn. asset: AssetId, + /// The SubId of the vault. + sub_id: SubId, /// The amount of assets being withdrawn. assets: u64, /// The amount of shares being burned. @@ -38,6 +42,7 @@ abi SRC6 { /// # Arguments /// /// * `receiver`: [Identity] - The receiver of the shares. + /// * `sub_id`: [SubId] - The SubId of the vault. /// /// # Returns /// @@ -49,7 +54,7 @@ abi SRC6 { /// * If the amount of assets forwarded to the contract is zero. /// * The user crosses any global or user specific deposit limits. #[storage(read, write)] - fn deposit(receiver: Identity) -> u64; + fn deposit(receiver: Identity, sub_id: SubId) -> u64; /// Burns shares from the sender and transfers assets to the receiver. /// @@ -60,6 +65,7 @@ abi SRC6 { /// # Arguments /// /// * `asset`: [AssetId] - The asset for which the shares should be burned. + /// * `sub_id`: [SubId] - The SubId of the vault. /// * `receiver`: [Identity] - The receiver of the assets. /// /// # Returns @@ -73,25 +79,27 @@ abi SRC6 { /// * If the transferred shares do not corresspond to the given asset. /// * The user crosses any global or user specific withdrawal limits. #[storage(read, write)] - fn withdraw(asset: AssetId, receiver: Identity) -> u64; + fn withdraw(asset: AssetId, sub_id: SubId, receiver: Identity) -> u64; /// Returns the amount of managed assets of the given asset. /// /// # Arguments /// /// * `asset`: [AssetId] - The asset for which the amount of managed assets should be returned. + /// * `sub_id`: [SubId] - The SubId of the vault. /// /// # Returns /// /// * [u64] - The amount of managed assets of the given asset. #[storage(read)] - fn managed_assets(asset: AssetId) -> u64; + fn managed_assets(asset: AssetId, sub_id: SubId) -> u64; /// Returns how many shares would be minted for the given amount of assets, in an ideal scenario (No accounting for slippage, or any limits). /// /// # Arguments /// /// * `asset`: [AssetId] - The asset for which the amount of shares should be returned. + /// * `sub_id`: [SubId] - The SubId of the vault. /// * `assets`: [u64] - The amount of assets for which the amount of shares should be returned. /// /// # Returns @@ -99,13 +107,14 @@ abi SRC6 { /// * [Some(u64)] - The amount of shares that would be minted for the given amount of assets. /// * [None] - If the asset is not supported by the contract. #[storage(read)] - fn convert_to_shares(asset: AssetId, assets: u64) -> Option; + fn convert_to_shares(asset: AssetId, sub_id: SubId, assets: u64) -> Option; /// Returns how many assets would be transferred for the given amount of shares, in an ideal scenario (No accounting for slippage, or any limits). /// /// # Arguments /// /// * `asset`: [AssetId] - The asset for which the amount of assets should be returned. + /// * `sub_id`: [SubId] - The SubId of the vault. /// * `shares`: [u64] - The amount of shares for which the amount of assets should be returned. /// /// # Returns @@ -113,7 +122,7 @@ abi SRC6 { /// * [Some(u64)] - The amount of assets that would be transferred for the given amount of shares. /// * [None] - If the asset is not supported by the contract. #[storage(read)] - fn convert_to_assets(asset: AssetId, shares: u64) -> Option; + fn convert_to_assets(asset: AssetId, sub_id: SubId, shares: u64) -> Option; /// Returns the maximum amount of assets that can be deposited into the contract, for the given asset. /// @@ -124,13 +133,14 @@ abi SRC6 { /// # Arguments /// /// * `asset`: [AssetId] - The asset for which the maximum amount of depositable assets should be returned. + /// * `sub_id`: [SubId] - The SubId of the vault. /// /// # Returns /// /// * [Some(u64)] - The maximum amount of assets that can be deposited into the contract, for the given asset. /// * [None] - If the asset is not supported by the contract. #[storage(read)] - fn max_depositable(asset: AssetId) -> Option; + fn max_depositable(asset: AssetId, sub_id: SubId) -> Option; /// Returns the maximum amount of assets that can be withdrawn from the contract, for the given asset. /// @@ -141,11 +151,39 @@ abi SRC6 { /// # Arguments /// /// * `asset`: [AssetId] - The asset for which the maximum amount of withdrawable assets should be returned. + /// * `sub_id`: [SubId] - The SubId of the vault. /// /// # Returns /// /// * [Some(u64)] - The maximum amount of assets that can be withdrawn from the contract, for the given asset. /// * [None] - If the asset is not supported by the contract. #[storage(read)] - fn max_withdrawable(asset: AssetId) -> Option; + fn max_withdrawable(asset: AssetId, sub_id: SubId) -> Option; + + /// Returns the AssetId of the vault shares for the given asset and sub vault. + /// + /// # Arguments + /// + /// * `asset`: [AssetId] - The asset for which the vault shares should be returned. + /// * `sub_id`: [SubId] - The SubId of the vault. + /// + /// # Returns + /// + /// * [Some(AssetId)] - The AssetId of the vault shares for the given asset and sub vault. + /// * [None] - If the asset is not supported by the contract. + #[storage(read)] + fn vault_asset_id(asset: AssetId, sub_id: SubId) -> Option; + + /// Returns the AssetId of the asset of the vault for the given AssetId of the vault shares. + /// + /// # Arguments + /// + /// * `vault_asset_id`: [AssetId] - The AssetId of the vault shares for which the asset of the vault should be returned. + /// + /// # Returns + /// + /// * [Some(AssetId)] - The AssetId of the asset of the vault for the given AssetId of the vault shares. + /// * [None] - If the asset is not supported by the contract or the vault has not been initialised. + #[storage(read)] + fn asset_of_vault(vault_asset_id: AssetId) -> Option; }