Skip to content

Commit

Permalink
Merge pull request #28 from FuelLabs/vault-standard-with-subvaults
Browse files Browse the repository at this point in the history
Vault standard with subvaults
  • Loading branch information
SwayStar123 authored Oct 31, 2023
2 parents 0a25bf9 + 0573079 commit 56f2a44
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 24 deletions.
2 changes: 1 addition & 1 deletion standards/Forc.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[workspace]
members = ["src_3", "src_5", "src_20"]
members = ["src_3", "src_5", "src_6", "src_20"]
52 changes: 36 additions & 16 deletions standards/src_6/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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.
Expand All @@ -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<u64>`
### `fn convert_to_shares(asset: AssetId, sub_id: SubId, assets: u64) -> Option<u64>`
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<u64>`
### `fn convert_to_assets(asset: AssetId, sub_id: SubId, shares: u64) -> Option<u64>`
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<u64>`
### `fn max_depositable(asset: AssetId, sub_id: SubId) -> Option<u64>`
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<u64>`
### `fn max_withdrawable(asset: AssetId, sub_id: SubId) -> Option<u64>`
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<AssetId>`
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<AssetId>`
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

Expand All @@ -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

Expand All @@ -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

Expand Down
185 changes: 185 additions & 0 deletions standards/src_6/examples/simple_vault.sw
Original file line number Diff line number Diff line change
@@ -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<AssetId, u64> = StorageMap {},
name: StorageMap<AssetId, StorageString> = StorageMap {},
symbol: StorageMap<AssetId, StorageString> = StorageMap {},
decimals: StorageMap<AssetId, u8> = 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<u64> {
Option::Some(preview_deposit(asset, assets))
}

#[storage(read)]
fn convert_to_assets(asset: AssetId, shares: u64) -> Option<u64> {
Option::Some(preview_withdraw(asset, shares))
}

#[storage(read)]
fn max_depositable(asset: AssetId) -> Option<u64> {
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<u64> {
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<u64> {
storage.total_supply.get(asset).try_read()
}

#[storage(read)]
fn name(asset: AssetId) -> Option<String> {
storage.name.get(asset).read_slice()
}

#[storage(read)]
fn symbol(asset: AssetId) -> Option<String> {
storage.symbol.get(asset).read_slice()
}

#[storage(read)]
fn decimals(asset: AssetId) -> Option<u8> {
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);
}
Loading

0 comments on commit 56f2a44

Please sign in to comment.