diff --git a/examples/src_6/multi_token_vault/.gitignore b/examples/src_6/multi_token_vault/.gitignore new file mode 100644 index 0000000..77d3844 --- /dev/null +++ b/examples/src_6/multi_token_vault/.gitignore @@ -0,0 +1,2 @@ +out +target diff --git a/examples/src_6/multi_token_vault/Forc.toml b/examples/src_6/multi_token_vault/Forc.toml new file mode 100644 index 0000000..7451256 --- /dev/null +++ b/examples/src_6/multi_token_vault/Forc.toml @@ -0,0 +1,9 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "multi_token_vault" + +[dependencies] +src_20 = { path = "../../../standards/src_20" } +src_6 = { path = "../../../standards/src_6" } diff --git a/examples/src_6/multi_token_vault/src/main.sw b/examples/src_6/multi_token_vault/src/main.sw new file mode 100644 index 0000000..3648807 --- /dev/null +++ b/examples/src_6/multi_token_vault/src/main.sw @@ -0,0 +1,245 @@ +contract; + +use std::{ + call_frames::msg_asset_id, + context::msg_amount, + hash::{ + Hash, + sha256, + }, + storage::storage_string::*, + string::String, + token::transfer, +}; + +use src_6::{Deposit, SRC6, Withdraw}; +use src_20::SRC20; + +pub struct VaultInfo { + /// Amount of assets currently managed by this vault + managed_assets: u64, + /// The vault_sub_id of this vault. + vault_sub_id: SubId, + /// The asset being managed by this vault + asset: AssetId, +} + +storage { + /// Vault share AssetId -> VaultInfo. + vault_info: StorageMap = StorageMap {}, + /// Number of different assets managed by this contract. + total_assets: u64 = 0, + /// Total supply of shares for each asset. + total_supply: StorageMap = StorageMap {}, + /// Asset name. + name: StorageMap = StorageMap {}, + /// Asset symbol. + symbol: StorageMap = StorageMap {}, + /// Asset decimals. + decimals: StorageMap = StorageMap {}, +} + +impl SRC6 for Contract { + #[storage(read, write)] + fn deposit(receiver: Identity, vault_sub_id: SubId) -> u64 { + let asset_amount = msg_amount(); + require(asset_amount != 0, "ZERO_ASSETS"); + + let underlying_asset = msg_asset_id(); + let (shares, share_asset, share_asset_vault_sub_id) = preview_deposit(underlying_asset, vault_sub_id, asset_amount); + + _mint(receiver, share_asset, share_asset_vault_sub_id, shares); + storage + .total_supply + .insert( + share_asset, + storage + .total_supply + .get(share_asset) + .read() + shares, + ); + + let mut vault_info = storage.vault_info.get(share_asset).read(); + vault_info.managed_assets = vault_info + .managed_assets + asset_amount; + storage.vault_info.insert(share_asset, vault_info); + + log(Deposit { + caller: msg_sender().unwrap(), + receiver, + underlying_asset, + vault_sub_id, + deposited_assets: asset_amount, + minted_shares, + }); + + shares + } + + #[storage(read, write)] + fn withdraw( + receiver: Identity, + underlying_asset: AssetId, + vault_sub_id: SubId, + ) -> u64 { + let shares = msg_amount(); + require(shares != 0, "ZERO_SHARES"); + + let (share_asset_id, share_asset_vault_sub_id) = vault_asset_id(underlying_asset, vault_sub_id); + + require(msg_asset_id() == share_asset_id, "INVALID_ASSET_ID"); + let assets = preview_withdraw(share_asset_id, shares); + + _burn(share_asset_id, share_asset_vault_sub_id, shares); + storage + .total_supply + .insert( + share_asset_id, + storage + .total_supply + .get(share_asset_id) + .read() - shares, + ); + + transfer(receiver, underlying_asset, assets); + + log(Withdraw { + caller: msg_sender().unwrap(), + receiver, + underlying_asset, + vault_sub_id, + withdrawn_assets: assets, + burned_shares: shares, + }); + + assets + } + #[storage(read)] + fn managed_assets(underlying_asset: AssetId, vault_sub_id: SubId) -> u64 { + let vault_share_asset = vault_asset_id(underlying_asset, vault_sub_id).0; + // 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. + managed_assets(vault_share_asset) + } + + #[storage(read)] + fn max_depositable( + receiver: Identity, + underlying_asset: AssetId, + vault_sub_id: SubId, + ) -> Option { + // This is the max value of u64 minus the current managed_assets. Ensures that the sum will always be lower than u64::MAX. + Some(u64::max() - managed_assets(underlying_asset)) + } + + #[storage(read)] + fn max_withdrawable(underlying_asset: AssetId, vault_sub_id: SubId) -> Option { + // 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. + Some(managed_assets(underlying_asset)) + } +} + +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() + } +} + +/// Returns the vault shares assetid and subid for the given assets assetid and the vaults sub id +fn vault_asset_id(asset: AssetId, vault_sub_id: SubId) -> (AssetId, SubId) { + let share_asset_vault_sub_id = sha256((asset, vault_sub_id)); + let share_asset_id = AssetId::new(ContractId::this(), share_asset_vault_sub_id); + (share_asset_id, share_asset_vault_sub_id) +} + +#[storage(read)] +fn managed_assets(share_asset: AssetId) -> u64 { + match storage.vault_info.get(share_asset).try_read() { + Some(vault_info) => vault_info.managed_assets, + None => 0, + } +} + +#[storage(read)] +fn preview_deposit( + underlying_asset: AssetId, + vault_sub_id: SubId, + assets: u64, +) -> (u64, AssetId, SubId) { + let (share_asset_id, share_asset_vault_sub_id) = vault_asset_id(underlying_asset, vault_sub_id); + + let shares_supply = storage.total_supply.get(share_asset_id).try_read().unwrap_or(0); + if shares_supply == 0 { + (assets, share_asset_id, share_asset_vault_sub_id) + } else { + ( + assets * shares_supply / managed_assets(share_asset_id), + share_asset_id, + share_asset_vault_sub_id, + ) + } +} + +#[storage(read)] +fn preview_withdraw(share_asset_id: AssetId, shares: u64) -> u64 { + let supply = storage.total_supply.get(share_asset_id).read(); + if supply == shares { + managed_assets(share_asset_id) + } else { + shares * (managed_assets(share_asset_id) / supply) + } +} + +#[storage(read, write)] +pub fn _mint( + recipient: Identity, + asset_id: AssetId, + vault_sub_id: SubId, + amount: u64, +) { + use std::token::mint_to; + + let supply = storage.total_supply.get(asset_id).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(storage.total_assets.read() + 1); + } + storage + .total_supply + .insert(asset_id, supply.unwrap_or(0) + amount); + mint_to(recipient, vault_sub_id, amount); +} + +#[storage(read, write)] +pub fn _burn(asset_id: AssetId, vault_sub_id: SubId, amount: u64) { + use std::{context::this_balance, token::burn}; + + 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_id).try_read().unwrap(); + storage.total_supply.insert(asset_id, supply - amount); + burn(vault_sub_id, amount); +} diff --git a/examples/src_6/single_token_single_sub_vault/.gitignore b/examples/src_6/single_token_single_sub_vault/.gitignore new file mode 100644 index 0000000..77d3844 --- /dev/null +++ b/examples/src_6/single_token_single_sub_vault/.gitignore @@ -0,0 +1,2 @@ +out +target diff --git a/examples/src_6/single_token_single_sub_vault/Forc.toml b/examples/src_6/single_token_single_sub_vault/Forc.toml new file mode 100644 index 0000000..6446f7a --- /dev/null +++ b/examples/src_6/single_token_single_sub_vault/Forc.toml @@ -0,0 +1,9 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "single_token_single_sub_vault" + +[dependencies] +src_20 = { path = "../../../standards/src_20" } +src_6 = { path = "../../../standards/src_6" } diff --git a/examples/src_6/single_token_single_sub_vault/src/main.sw b/examples/src_6/single_token_single_sub_vault/src/main.sw new file mode 100644 index 0000000..f20be31 --- /dev/null +++ b/examples/src_6/single_token_single_sub_vault/src/main.sw @@ -0,0 +1,217 @@ +contract; + +use std::{ + call_frames::msg_asset_id, + constants::{ + BASE_ASSET_ID, + ZERO_B256, + }, + context::msg_amount, + hash::{ + Hash, + sha256, + }, + storage::storage_string::*, + string::String, + token::transfer, +}; + +use src_6::{Deposit, SRC6, Withdraw}; +use src_20::SRC20; + +configurable { + /// The only asset that can be deposited and withdrawn from this vault. + ACCEPTED_TOKEN: AssetId = BASE_ASSET_ID, + /// The only sub vault that can be deposited and withdrawn from this vault. + ACCEPTED_SUB_VAULT: SubId = ZERO_B256, + PRE_CALCULATED_SHARE_VAULT_SUB_ID: SubId = 0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b, +} + +storage { + /// The total amount of assets managed by this vault. + managed_assets: u64 = 0, + /// The total amount of shares minted by this vault. + total_supply: u64 = 0, + /// Whether the vault shares have been minted. + minted: bool = false, +} + +impl SRC6 for Contract { + #[storage(read, write)] + fn deposit(receiver: Identity, vault_sub_id: SubId) -> u64 { + require(vault_sub_id == ACCEPTED_SUB_VAULT, "INVALID_vault_sub_id"); + + let underlying_asset = msg_asset_id(); + require(underlying_asset == ACCEPTED_TOKEN, "INVALID_ASSET_ID"); + + let asset_amount = msg_amount(); + require(asset_amount != 0, "ZERO_ASSETS"); + let shares = preview_deposit(asset_amount); + + _mint(receiver, shares); + storage + .total_supply + .write(storage.total_supply.read() + shares); + + storage + .managed_assets + .write(storage.managed_assets.read() + asset_amount); + + log(Deposit { + caller: msg_sender().unwrap(), + receiver, + underlying_asset, + vault_sub_id, + deposited_assets: asset_amount, + minted_shares: shares, + }); + + shares + } + + #[storage(read, write)] + fn withdraw( + receiver: Identity, + underlying_asset: AssetId, + vault_sub_id: SubId, + ) -> u64 { + require(underlying_asset == ACCEPTED_TOKEN, "INVALID_ASSET_ID"); + require(vault_sub_id == ACCEPTED_SUB_VAULT, "INVALID_vault_sub_id"); + + let shares = msg_amount(); + require(shares != 0, "ZERO_SHARES"); + + let share_asset_id = vault_assetid(); + + require(msg_asset_id() == share_asset_id, "INVALID_ASSET_ID"); + let assets = preview_withdraw(shares); + + _burn(share_asset_id, shares); + storage + .total_supply + .write(storage.total_supply.read() - shares); + + transfer(receiver, underlying_asset, assets); + + log(Withdraw { + caller: msg_sender().unwrap(), + receiver, + underlying_asset, + vault_sub_id, + withdrawn_assets: assets, + burned_shares: shares, + }); + + assets + } + + #[storage(read)] + fn managed_assets(underlying_asset: AssetId, vault_sub_id: SubId) -> u64 { + if underlying_asset == ACCEPTED_TOKEN && vault_sub_id == ACCEPTED_SUB_VAULT { + // 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.managed_assets.read() + } else { + 0 + } + } + + #[storage(read)] + fn max_depositable( + receiver: Identity, + underlying_asset: AssetId, + vault_sub_id: SubId, + ) -> Option { + if underlying_asset == ACCEPTED_TOKEN { + // This is the max value of u64 minus the current managed_assets. Ensures that the sum will always be lower than u64::MAX. + Some(u64::max() - storage.managed_assets.read()) + } else { + None + } + } + + #[storage(read)] + fn max_withdrawable(underlying_asset: AssetId, vault_sub_id: SubId) -> Option { + if underlying_asset == ACCEPTED_TOKEN { + // 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. + Some(storage.managed_assets.read()) + } else { + None + } + } +} + +impl SRC20 for Contract { + #[storage(read)] + fn total_assets() -> u64 { + 1 + } + + #[storage(read)] + fn total_supply(asset: AssetId) -> Option { + Some(storage.total_supply.read()) + } + + #[storage(read)] + fn name(asset: AssetId) -> Option { + Some(String::from_ascii_str("Vault Shares")) + } + + #[storage(read)] + fn symbol(asset: AssetId) -> Option { + Some(String::from_ascii_str("VLTSHR")) + } + + #[storage(read)] + fn decimals(asset: AssetId) -> Option { + Some(9_u8) + } +} + +/// Returns the vault shares assetid for the given assets assetid and the vaults sub id +fn vault_assetid() -> AssetId { + let share_asset_id = AssetId::new(ContractId::this(), PRE_CALCULATED_SHARE_VAULT_SUB_ID); + share_asset_id +} + +#[storage(read)] +fn preview_deposit(assets: u64) -> u64 { + let shares_supply = storage.total_supply.try_read().unwrap_or(0); + if shares_supply == 0 { + assets + } else { + assets * shares_supply / storage.managed_assets.try_read().unwrap_or(0) + } +} + +#[storage(read)] +fn preview_withdraw(shares: u64) -> u64 { + let supply = storage.total_supply.read(); + if supply == shares { + storage.managed_assets.read() + } else { + shares * (storage.managed_assets.read() / supply) + } +} + +#[storage(read, write)] +pub fn _mint(recipient: Identity, amount: u64) { + use std::token::mint_to; + + let supply = storage.total_supply.read(); + storage.total_supply.write(supply + amount); + mint_to(recipient, PRE_CALCULATED_SHARE_VAULT_SUB_ID, amount); +} + +#[storage(read, write)] +pub fn _burn(asset_id: AssetId, amount: u64) { + use std::{context::this_balance, token::burn}; + + 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.read(); + storage.total_supply.write(supply - amount); + burn(PRE_CALCULATED_SHARE_VAULT_SUB_ID, amount); +} diff --git a/examples/src_6/single_token_vault/.gitignore b/examples/src_6/single_token_vault/.gitignore new file mode 100644 index 0000000..77d3844 --- /dev/null +++ b/examples/src_6/single_token_vault/.gitignore @@ -0,0 +1,2 @@ +out +target diff --git a/examples/src_6/single_token_vault/Forc.toml b/examples/src_6/single_token_vault/Forc.toml new file mode 100644 index 0000000..72a79eb --- /dev/null +++ b/examples/src_6/single_token_vault/Forc.toml @@ -0,0 +1,9 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "single_token_vault" + +[dependencies] +src_20 = { path = "../../../standards/src_20" } +src_6 = { path = "../../../standards/src_6" } diff --git a/examples/src_6/single_token_vault/src/main.sw b/examples/src_6/single_token_vault/src/main.sw new file mode 100644 index 0000000..06c7270 --- /dev/null +++ b/examples/src_6/single_token_vault/src/main.sw @@ -0,0 +1,266 @@ +contract; + +use std::{ + call_frames::msg_asset_id, + constants::BASE_ASSET_ID, + context::msg_amount, + hash::{ + Hash, + sha256, + }, + storage::storage_string::*, + string::String, + token::transfer, +}; + +use src_6::{Deposit, SRC6, Withdraw}; +use src_20::SRC20; + +pub struct VaultInfo { + /// Amount of assets currently managed by this vault + managed_assets: u64, + /// The vault_sub_id of this vault. + vault_sub_id: SubId, + /// The asset being managed by this vault + asset: AssetId, +} + +storage { + /// Vault share AssetId -> VaultInfo. + vault_info: StorageMap = StorageMap {}, + /// Number of different assets managed by this contract. + total_assets: u64 = 0, + /// Total supply of shares. + total_supply: StorageMap = StorageMap {}, + /// Asset name. + name: StorageMap = StorageMap {}, + /// Asset symbol. + symbol: StorageMap = StorageMap {}, + /// Asset decimals. + decimals: StorageMap = StorageMap {}, +} + +configurable { + /// The only asset that can be deposited and withdrawn from this vault. + ACCEPTED_TOKEN: AssetId = BASE_ASSET_ID, +} + +impl SRC6 for Contract { + #[storage(read, write)] + fn deposit(receiver: Identity, vault_sub_id: SubId) -> u64 { + let asset_amount = msg_amount(); + let underlying_asset = msg_asset_id(); + + require(underlying_asset == ACCEPTED_TOKEN, "INVALID_ASSET_ID"); + let (shares, share_asset, share_asset_vault_sub_id) = preview_deposit(underlying_asset, vault_sub_id, asset_amount); + require(asset_amount != 0, "ZERO_ASSETS"); + + _mint(receiver, share_asset, share_asset_vault_sub_id, shares); + storage + .total_supply + .insert( + share_asset, + storage + .total_supply + .get(share_asset) + .read() + shares, + ); + + let mut vault_info = storage.vault_info.get(share_asset).read(); + vault_info.managed_assets = vault_info + .managed_assets + asset_amount; + storage.vault_info.insert(share_asset, vault_info); + + log(Deposit { + caller: msg_sender().unwrap(), + receiver: receiver, + underlying_asset, + vault_sub_id: vault_sub_id, + deposited_assets: asset_amount, + minted_shares: shares, + }); + + shares + } + + #[storage(read, write)] + fn withdraw( + receiver: Identity, + underlying_asset: AssetId, + vault_sub_id: SubId, + ) -> u64 { + let shares = msg_amount(); + require(shares != 0, "ZERO_SHARES"); + + let (share_asset_id, share_asset_vault_sub_id) = vault_asset_id(underlying_asset, vault_sub_id); + + require(msg_asset_id() == share_asset_id, "INVALID_ASSET_ID"); + let assets = preview_withdraw(share_asset_id, shares); + + _burn(share_asset_id, share_asset_vault_sub_id, shares); + storage + .total_supply + .insert( + share_asset_id, + storage + .total_supply + .get(share_asset_id) + .read() - shares, + ); + + transfer(receiver, underlying_asset, assets); + + log(Withdraw { + caller: msg_sender().unwrap(), + receiver: receiver, + underlying_asset, + vault_sub_id: vault_sub_id, + withdrawn_assets: assets, + burned_shares: shares, + }); + + assets + } + + #[storage(read)] + fn managed_assets(underlying_asset: AssetId, vault_sub_id: SubId) -> u64 { + if underlying_asset == ACCEPTED_TOKEN { + let vault_share_asset = vault_asset_id(underlying_asset, vault_sub_id).0; + // 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. + managed_assets(vault_share_asset) + } else { + 0 + } + } + + #[storage(read)] + fn max_depositable( + receiver: Identity, + underlying_asset: AssetId, + vault_sub_id: SubId, + ) -> Option { + if underlying_asset == ACCEPTED_TOKEN { + // This is the max value of u64 minus the current managed_assets. Ensures that the sum will always be lower than u64::MAX. + Some(u64::max() - managed_assets(underlying_asset)) + } else { + None + } + } + + #[storage(read)] + fn max_withdrawable(underlying_asset: AssetId, vault_sub_id: SubId) -> Option { + if underlying_asset == ACCEPTED_TOKEN { + // 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. + Some(managed_assets(underlying_asset)) + } else { + None + } + } +} + +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() + } +} + +/// Returns the vault shares assetid and subid for the given assets assetid and the vaults sub id +fn vault_asset_id(underlying_asset: AssetId, vault_sub_id: SubId) -> (AssetId, SubId) { + let share_asset_vault_sub_id = sha256((underlying_asset, vault_sub_id)); + let share_asset_id = AssetId::new(ContractId::this(), share_asset_vault_sub_id); + (share_asset_id, share_asset_vault_sub_id) +} + +#[storage(read)] +fn managed_assets(share_asset: AssetId) -> u64 { + match storage.vault_info.get(share_asset).try_read() { + Some(vault_info) => vault_info.managed_assets, + None => 0, + } +} + +#[storage(read)] +fn preview_deposit( + underlying_asset: AssetId, + vault_sub_id: SubId, + assets: u64, +) -> (u64, AssetId, SubId) { + let (share_asset_id, share_asset_vault_sub_id) = vault_asset_id(underlying_asset, vault_sub_id); + + let shares_supply = storage.total_supply.get(share_asset_id).try_read().unwrap_or(0); + if shares_supply == 0 { + (assets, share_asset_id, share_asset_vault_sub_id) + } else { + ( + assets * shares_supply / managed_assets(share_asset_id), + share_asset_id, + share_asset_vault_sub_id, + ) + } +} + +#[storage(read)] +fn preview_withdraw(share_asset_id: AssetId, shares: u64) -> u64 { + let supply = storage.total_supply.get(share_asset_id).read(); + if supply == shares { + managed_assets(share_asset_id) + } else { + shares * (managed_assets(share_asset_id) / supply) + } +} + +#[storage(read, write)] +pub fn _mint( + recipient: Identity, + asset_id: AssetId, + vault_sub_id: SubId, + amount: u64, +) { + use std::token::mint_to; + + let supply = storage.total_supply.get(asset_id).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(storage.total_assets.read() + 1); + } + let current_supply = supply.unwrap_or(0); + storage + .total_supply + .insert(asset_id, current_supply + amount); + mint_to(recipient, vault_sub_id, amount); +} + +#[storage(read, write)] +pub fn _burn(asset_id: AssetId, vault_sub_id: SubId, amount: u64) { + use std::{context::this_balance, token::burn}; + + 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_id).try_read().unwrap(); + storage.total_supply.insert(asset_id, supply - amount); + burn(vault_sub_id, amount); +} diff --git a/standards/Forc.toml b/standards/Forc.toml index fced462..67c80e8 100644 --- a/standards/Forc.toml +++ b/standards/Forc.toml @@ -1,2 +1,2 @@ [workspace] -members = ["src_3", "src_5", "src_7", "src_20"] +members = ["src_3", "src_5", "src_6", "src_7", "src_20"] diff --git a/standards/src_3/README.md b/standards/src_3/README.md index e4d6d30..3a582e3 100644 --- a/standards/src_3/README.md +++ b/standards/src_3/README.md @@ -16,27 +16,27 @@ Minting and burning were initially added to the [SRC-20](https://github.com/Fuel The following functions MUST be implemented to follow the SRC-3 standard: -### `fn mint(recipient: Identity, sub_id: SubId, amount: u64)` +### `fn mint(recipient: Identity, vault_sub_id: SubId, amount: u64)` -This function MUST mint `amount` tokens with sub-identifier `sub_id` and transfer them to the `recipient`. +This function MUST mint `amount` tokens with sub-identifier `vault_sub_id` and transfer them to the `recipient`. This function MAY contain arbitrary conditions for minting, and revert if those conditions are not met. ##### Arguments * `recipient` - The `Identity` to which the newly minted tokens are transferred to. -* `sub_id` - The sub-identifier of the asset to mint. +* `vault_sub_id` - The sub-identifier of the asset to mint. * `amount` - The quantity of tokens to mint. -### `fn burn(sub_id: SubId, amount: u64)` +### `fn burn(vault_sub_id: SubId, amount: u64)` -This function MUST burn `amount` tokens with the sub-identifier `sub_id` and MUST ensure the `AssetId` of the token is the sha-256 hash of `(ContractId, SubId)` for the implementing contract. +This function MUST burn `amount` tokens with the sub-identifier `vault_sub_id` and MUST ensure the `AssetId` of the token is the sha-256 hash of `(ContractId, SubId)` for the implementing contract. This function MUST ensure at least `amount` tokens have been transferred to the implementing contract. This function MUST update the total supply defined in the [SRC-20](https://github.com/FuelLabs/sway-standards/tree/master/standards/src_20) standard. This function MAY contain arbitrary conditions for burning, and revert if those conditions are not met. ##### Arguments -* `sub_id` - The sub-identifier of the asset to burn. +* `vault_sub_id` - The sub-identifier of the asset to burn. * `amount` - The quantity of tokens to burn. # Rationale @@ -56,8 +56,8 @@ The burn function may also introduce a security consideration if the total suppl ```rust abi MySRC3Token { - fn mint(recipient: Identity, sub_id: SubId, amount: u64); - fn burn(sub_id: SubId, amount: u64); + fn mint(recipient: Identity, vault_sub_id: SubId, amount: u64); + fn burn(vault_sub_id: SubId, amount: u64); } ``` diff --git a/standards/src_3/src/src_3.sw b/standards/src_3/src/src_3.sw index 987be34..4383811 100644 --- a/standards/src_3/src/src_3.sw +++ b/standards/src_3/src/src_3.sw @@ -1,12 +1,12 @@ library; abi SRC3 { - /// Mints new tokens using the `sub_id` sub-identifier. + /// Mints new tokens using the `vault_sub_id` sub-identifier. /// /// # Arguments /// /// * `recipient`: [Identity] - The user to which the newly minted tokens are transferred to. - /// * `sub_id`: [SubId] - The sub-identifier of the newly minted token. + /// * `vault_sub_id`: [SubId] - The sub-identifier of the newly minted token. /// * `amount`: [u64] - The quantity of tokens to mint. /// /// # Examples @@ -20,18 +20,18 @@ abi SRC3 { /// } /// ``` #[storage(read, write)] - fn mint(recipient: Identity, sub_id: SubId, amount: u64); + fn mint(recipient: Identity, vault_sub_id: SubId, amount: u64); - /// Burns tokens sent with the given `sub_id`. + /// Burns tokens sent with the given `vault_sub_id`. /// /// # Additional Information /// /// NOTE: The sha-256 hash of `(ContractId, SubId)` must match the `AssetId` where `ContractId` is the id of - /// the implementing contract and `SubId` is the given `sub_id` argument. + /// the implementing contract and `SubId` is the given `vault_sub_id` argument. /// /// # Arguments /// - /// * `sub_id`: [SubId] - The sub-identifier of the token to burn. + /// * `vault_sub_id`: [SubId] - The sub-identifier of the token to burn. /// * `amount`: [u64] - The quantity of tokens to burn. /// /// # Examples @@ -49,5 +49,5 @@ abi SRC3 { /// } /// ``` #[storage(read, write)] - fn burn(sub_id: SubId, amount: u64); + fn burn(vault_sub_id: SubId, amount: u64); } diff --git a/standards/src_6/.docs/src-6-logo-dark-theme.png b/standards/src_6/.docs/src-6-logo-dark-theme.png new file mode 100644 index 0000000..5795bb9 Binary files /dev/null and b/standards/src_6/.docs/src-6-logo-dark-theme.png differ diff --git a/standards/src_6/.docs/src-6-logo-light-theme.png b/standards/src_6/.docs/src-6-logo-light-theme.png new file mode 100644 index 0000000..f635d2a Binary files /dev/null and b/standards/src_6/.docs/src-6-logo-light-theme.png differ diff --git a/standards/src_6/Forc.toml b/standards/src_6/Forc.toml new file mode 100644 index 0000000..7beb7a0 --- /dev/null +++ b/standards/src_6/Forc.toml @@ -0,0 +1,5 @@ +[project] +authors = ["Fuel Labs "] +entry = "src_6.sw" +license = "Apache-2.0" +name = "src_6" diff --git a/standards/src_6/README.md b/standards/src_6/README.md new file mode 100644 index 0000000..6114f24 --- /dev/null +++ b/standards/src_6/README.md @@ -0,0 +1,185 @@ +

+ + + SRC-6 logo + +

+ +# Abstract + +The following standard allows for the implementation of a standard API for token vaults such as yield-bearing token vaults or asset wrappers. This standard is an optional add-on to the [SRC-20](https://github.com/FuelLabs/sway-standards/tree/master/standards/src_20) standard. + +# Motivation + +Token vaults allow users to own shares of variable amounts of assets, such as lending protocols which may have growing assets due to profits from interest. This pattern is highly useful and would greatly benefit from standardization. + +# Prior Art + +Token vaults have been thoroughly explored on Ethereum and with [EIP 4626](https://eips.ethereum.org/EIPS/eip-4626) they have their own standard for it. However as Fuel's [Native Assets](https://docs.fuel.network/docs/sway/blockchain-development/native_assets) are fundamentally different from Ethereum's ERC-20 tokens, the implementation will differ, but the interface may be used as a reference. + +# Specification + +## Required public functions + +The following functions MUST be implemented to follow the SRC-6 standard. Any contract that implements the SRC-6 standard MUST implement the SRC-20 standard. + +### `fn deposit(receiver: Identity, vault_sub_id: SubId) -> u64` + +This function takes the `receiver` Identity and the SubId `vault_sub_id` of the sub-vault as an argument and returns the amount of shares minted to the `receiver`. + +- This function MUST allow for depositing of the underlying asset in exchange for pro-rata shares of the vault. +- This function MAY reject arbitrary assets based on implementation and MUST revert if unaccepted assets are forwarded. +- This function MUST mint an asset representing the pro-rata share of the vault, with the SubId of the `sha256((underlying_asset, vault_sub_id))` digest, where `underlying_asset` is the AssetId of the deposited asset and the `vault_sub_id` is the id of the vault. +- This function MUST emit a `Deposit` log. +- This function MUST return the amount of minted shares. + +### `fn withdraw(receiver: Identity, underlying_asset: AssetId, vault_sub_id: SubId) -> u64` + +This function takes the `receiver` Identity, the `underlying_asset` AssetId, and the `vault_sub_id` of the sub vault, as arguments and returns the amount of assets transferred to the `receiver`. + +- This function MUST allow for redeeming of the vault shares in exchange for a pro-rata amount of the underlying assets. +- This function MUST revert if any AssetId other than the AssetId representing the underlying asset's shares for the given sub vault at `vault_sub_id` is forwarded. (i.e. transferred share's AssetId must be equal to `AssetId::new(ContractId::this(), sha256((underlying_asset, vault_sub_id))`) +- This function MUST burn the received shares. +- This function MUST emit a `Withdraw` log. +- This function MUST return amount of assets transferred to the receiver. + +### `fn managed_assets(underlying_asset: AssetId, vault_sub_id: SubId) -> u64` + +This function returns the total assets under management by vault. Includes assets controlled by the vault but not directly possessed by vault. It takes the `underlying_asset` AssetId and the `vault_sub_id` of the sub vault as arguments and returns the total amount of assets of AssetId under management by vault. + +- This function MUST return total amount of assets of `underlying_asset` AssetId under management by vault. +- This function MUST return 0 if there are no assets of `underlying_asset` AssetId under management by vault. +- This function MUST NOT revert under any circumstances. + +### `fn max_depositable(receiver: Identity, underlying_asset: AssetId, vault_sub_id: SubId) -> Option` + +This is a helper function for getting the maximum amount of assets that can be deposited. It takes the hypothetical `receiver` Identity, the `underlying_asset` AssetId, and the `vault_sub_id` SubId of the sub vault as an arguments and returns the maximum amount of assets that can be deposited into the contract, for the given asset. + +- This function MUST return the maximum amount of assets that can be deposited into the contract, for the given `underlying_asset`, if the given `vault_sub_id` vault exists. +- This function MUST return an `Some(amount)` if the given `vault_sub_id` vault exists. +- This function MUST return an `None` if the given `vault_sub_id` vault does not exist. +- This function MUST account for both global and user specific limits. For example: if deposits are disabled, even temporarily, MUST return 0. + +### `fn max_withdrawable(receiver: Identity, underlying_asset: AssetId, vault_sub_id: SubId) -> Option` + +This is a helper function for getting maximum withdrawable. It takes the hypothetical `receiver` Identity, the `underlying_asset` AssetId, and the `vault_sub_id` SubId 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. + +- This function MUST return the maximum amount of assets that can be withdrawn from the contract, for the given `underlying_asset`, if the given `vault_sub_id` vault exists. +- This function MUST return an `Some(amount)` if the given `vault_sub_id` vault exists. +- This function MUST return an `None` if the given `vault_sub_id` vault does not exist. +- This function MUST account for global limits. For example: if withdrawals are disabled, even temporarily, MUST return 0. + +## Required logs + +The following logs MUST be emitted at the specified occasions. + +### `Deposit` + +`caller` has called the `deposit()` method sending `deposited_amount` assets of the `underlying_asset` Asset to the subvault of `vault_sub_id`, in exchange for `minted_shares` shares sent to the receiver `receiver`. + +The `Deposit` struct MUST be logged whenever new shares are minted via the `deposit()` method. + +The `Deposit` log SHALL have the following fields. + +#### - `caller`: Identity + +The `caller` field MUST represent the Identity which called the deposit function. + +#### - `receiver`: Identity + +The `receiver` field MUST represent the Identity which received the vault shares. + +#### - `underlying_asset`: AssetId + +The `underlying_asset` field MUST represent the AssetId of the asset which was deposited into the vault. + +#### - `vault_sub_id`: SubId + +The `vault_sub_id` field MUST represent the SubId of the vault which was deposited into. + +#### - `deposited_amount`: u64 + +The `deposited_amount` field MUST represent the u64 amount of assets deposited into the vault. + +#### - `minted_shares`: u64 + +The `minted_shares` field MUST represent the u64 amount of shares minted. + +### `Withdraw` + +`caller` has called the `withdraw()` method sending `burned_shares` shares in exchange for `withdrawn_amount` assets of the `underlying_asset` Asset from the subvault of `vault_sub_id` to the receiver `receiver`. + +The `Withdraw` struct MUST be logged whenever shares are redeemed for assets via the `withdraw()` method. + +The `Withdraw` log SHALL have the following fields. + +#### - `caller`: Identity + +The `caller` field MUST represent the Identity which called the withdraw function. + +#### - `receiver`: Identity + +The `receiver` field MUST represent the Identity which received the withdrawn assets. + +#### - `underlying_asset`: AssetId + +The `underlying_asset` field MUST represent the AssetId of the asset that was withdrawn. + +#### - `vault_sub_id`: SubId + +The `vault_sub_id` field MUST represent the SubId of the vault from which was withdrawn. + +#### - `withdrawn_amount`: u64 + +The `withdrawn_amount` field MUST represent the u64 amount of tokens withdrawn. + +#### - `burned_shares`: u64 + +The `burned_shares` field MUST represent the u64 amount of shares burned. + +# Rationale + +The ABI discussed covers the known use cases of token vaults while allowing safe implementations. + +# Backwards Compatibility + +This standard is fully compatible with the [SRC-20 standard](https://github.com/FuelLabs/sway-standards/tree/master/standards/src_20). + +# Security Considerations + +Incorrect implementation of token vaults could allow attackers to steal underlying assets. It is recommended to properly audit any code using this standard to ensure exploits are not possible. + +# Example ABI + +```sway +abi SRC6 { + #[storage(read, write)] + fn deposit(receiver: Identity, vault_sub_id: SubId) -> u64; + + #[storage(read, write)] + fn withdraw(receiver: Identity, underlying_asset: AssetId, vault_sub_id: SubId) -> u64; + + #[storage(read)] + fn managed_assets(underlying_asset: AssetId, vault_sub_id: SubId) -> u64; + + #[storage(read)] + fn max_depositable(receiver: Identity, underlying_asset: AssetId, vault_sub_id: SubId) -> Option; + + #[storage(read)] + fn max_withdrawable(underlying_asset: AssetId, vault_sub_id: SubId) -> Option; +} +``` + +# Example Implementation + +## [Multi Token Vault](../../examples/src_6/multi_token_vault/) + +A barebones implementation of the vault standard that supports any number of sub vaults being created for every AssetId. + +## [Single Token Vault](../../examples/src_6/single_token_vault/) + +A barebones implemenation of the vault standard demonstrating how to constrict deposits and withdrawals to a single AssetId. + +## [Single Token Single Sub Vault](../../examples/src_6/single_token_single_sub_vault/) + +A barebones implementation of the vault standard demonstrating how to constrict deposits and withdrawals to a single AssetId, and to a single Sub vault. \ No newline at end of file diff --git a/standards/src_6/src/src_6.sw b/standards/src_6/src/src_6.sw new file mode 100644 index 0000000..3c9cbf5 --- /dev/null +++ b/standards/src_6/src/src_6.sw @@ -0,0 +1,141 @@ +library; + +/// Event logged when a deposit is made. +pub struct Deposit { + /// The caller of the deposit function. + caller: Identity, + /// The receiver of the deposit. + receiver: Identity, + /// The asset being deposited. + underlying_asset: AssetId, + /// The SubId of the vault. + vault_sub_id: SubId, + /// The amount of assets being deposited. + deposited_amount: u64, + /// The amount of shares being minted. + minted_shares: u64, +} + +/// Event logged when a withdrawal is made. +pub struct Withdraw { + /// The caller of the withdrawal function. + caller: Identity, + /// The receiver of the withdrawal. + receiver: Identity, + /// The asset being withdrawn. + underlying_asset: AssetId, + /// The SubId of the vault. + vault_sub_id: SubId, + /// The amount of assets being withdrawn. + withdrawn_amount: u64, + /// The amount of shares being burned. + burned_shares: u64, +} + +abi SRC6 { + /// Deposits assets into the contract and mints shares to the receiver. + /// + /// # Additional Information + /// + /// * Assets must be forwarded to the contract in the contract call. + /// + /// # Arguments + /// + /// * `receiver`: [Identity] - The receiver of the shares. + /// * `vault_sub_id`: [SubId] - The SubId of the vault. + /// + /// # Returns + /// + /// * [u64] - The amount of shares minted. + /// + /// # Reverts + /// + /// * If the asset is not supported by the contract. + /// * 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, vault_sub_id: SubId) -> u64; + + /// Burns shares from the sender and transfers assets to the receiver. + /// + /// # Additional Information + /// + /// * Shares must be forwarded to the contract in the contract call. + /// + /// # Arguments + /// + /// * `receiver`: [Identity] - The receiver of the assets. + /// * `underlying_asset`: [AssetId] - The asset for which the shares should be burned. + /// * `vault_sub_id`: [SubId] - The SubId of the vault. + /// + /// # Returns + /// + /// * [u64] - The amount of assets transferred. + /// + /// # Reverts + /// + /// * If the asset is not supported by the contract. + /// * If the amount of shares is zero. + /// * 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( + receiver: Identity, + underlying_asset: AssetId, + vault_sub_id: SubId, + ) -> u64; + + /// Returns the amount of managed assets of the given asset. + /// + /// # Arguments + /// + /// * `underlying_asset`: [AssetId] - The asset for which the amount of managed assets should be returned. + /// * `vault_sub_id`: [SubId] - The SubId of the vault. + /// + /// # Returns + /// + /// * [u64] - The amount of managed assets of the given asset. + #[storage(read)] + fn managed_assets(underlying_asset: AssetId, vault_sub_id: SubId) -> u64; + + /// Returns the maximum amount of assets that can be deposited into the contract, for the given asset. + /// + /// # Additional Information + /// + /// Must account for any user or global limits. + /// + /// # Arguments + /// + /// * `receiver`: [Identity] - The hypothetical receiver of the shares. + /// * `underlying_asset`: [AssetId] - The asset for which the maximum amount of depositable assets should be returned. + /// * `vault_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( + receiver: Identity, + underlying_asset: AssetId, + vault_sub_id: SubId, + ) -> Option; + + /// Returns the maximum amount of assets that can be withdrawn from the contract, for the given asset. + /// + /// # Additional Information + /// + /// Must account for any global limits. + /// + /// # Arguments + /// + /// * `underlying_asset`: [AssetId] - The asset for which the maximum amount of withdrawable assets should be returned. + /// * `vault_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(underlying_asset: AssetId, vault_sub_id: SubId) -> Option; +}