diff --git a/docs/book/spell-check-custom-words.txt b/docs/book/spell-check-custom-words.txt index ce6fe64966b..680eb4f7762 100644 --- a/docs/book/spell-check-custom-words.txt +++ b/docs/book/spell-check-custom-words.txt @@ -187,4 +187,5 @@ arity tuple's unary SRC -DEX \ No newline at end of file +DEX +SubId \ No newline at end of file diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 3db1d0b8ddc..786770abed2 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -11,6 +11,7 @@ - [Counter](./examples/counter.md) - [`FizzBuzz`](./examples/fizzbuzz.md) - [Wallet Smart Contract](./examples/wallet_smart_contract.md) + - [Liquidity Pool](./examples/liquidity_pool.md) - [Program Types](./sway-program-types/index.md) - [Contracts](./sway-program-types/smart_contracts.md) - [Libraries](./sway-program-types/libraries.md) diff --git a/docs/book/src/blockchain-development/native_assets.md b/docs/book/src/blockchain-development/native_assets.md index 6796e77cc45..143ac06adf7 100644 --- a/docs/book/src/blockchain-development/native_assets.md +++ b/docs/book/src/blockchain-development/native_assets.md @@ -1,32 +1,457 @@ -# Native Support for Multiple Asset Types +# Native Assets The FuelVM has built-in support for working with multiple assets. -What does this mean in practice? +## Key Differences Between EVM and FuelVM Assets -As in the EVM, sending ETH to an address or contract is an operation built into the FuelVM, meaning it doesn't rely on the existence of some token smart contract to update balances to track ownership. +### ERC-20 vs Native Asset -However, unlike the EVM, the process for sending _any_ native asset is the same. This means that while you would still need a smart contract to handle the minting and burning of fungible assets, the sending and receiving of these assets can be done independently of the asset contract. - +On the EVM, Ether is the native asset. As such, sending ETH to an address or contract is an operation built into the EVM, meaning it doesn't rely on the existence of a smart contract to update balances to track ownership as with ERC-20 tokens. + +On the FuelVM, _all_ assets are native and the process for sending _any_ native asset is the same. + +While you would still need a smart contract to handle the minting and burning of assets, the sending and receiving of these assets can be done independently of the asset contract. + +> **NOTE** It is important to note that Fuel does not have tokens. + +### No Token Approvals + +An advantage Native Assets bring is that there is no need for token approvals; as with Ether on the EVM. With millions of dollars hacked every year due to misused token approvals, the FuelVM eliminates this attack vector. + +### Asset vs Coin vs Token + +An "Asset" is a Native Asset on Fuel and has the associated `AssetId` type. Assets are distinguishable from one another. A "Coin" represents a singular unit of an Asset. Coins of the same Asset are not distinguishable from one another. + +Fuel does not use tokens like other ecosystems such as Ethereum and uses Native Assets with a UTXO design instead. + +## The `AssetId` type + +The `AssetId` type represents any Native Asset on Fuel. An `AssetId` is used for interacting with an asset on the network. + +The `AssetId` of any Native Asset on Fuel is calculated by taking the SHA256 hash digest of the originating `ContractId` that minted the asset and a `SubId` i.e. `sha256((contract_id, sub_id))`. + +### Creating a New `AssetId` + +There are 3 ways to instantiate a new `AssetId`: + +#### Default + +When a contract will only ever mint a single asset, it is recommended to use the `DEFAULT_ASSET_ID` sub id. This is referred to as the default asset of a contract. + +To get the default asset from an internal contract call, call the `default()` function: + +```sway +{{#include ../../../../examples/native_asset/src/main.sw:default_asset_id}} +``` + +#### New + +If a contract mints multiple assets or if the asset has been minted by an external contract, the `new()` function will be needed. The `new()` function takes the `ContractId` of the contract which minted the token as well as a `SubId`. + +To create a new `AssetId` using a `ContractId` and `SubId`, call the `new()` function: + +```sway +{{#include ../../../../examples/native_asset/src/main.sw:new_asset_id}} +``` + +#### From + +In the case where the `b256` value of an asset is already known, you may call the `from()` function with the `b256` value. + +```sway +{{#include ../../../../examples/native_asset/src/main.sw:from_asset_id}} +``` + +## The `SubId` type + +The SubId is used to differentiate between different assets that are created by the same contract. The `SubId` is a `b256` value. + +When creating an single new asset on Fuel, we recommend using the `DEFAULT_ASSET_ID`. + +## The Base Asset + +On the Fuel Network, the base asset is Ether. This is the only asset on the Fuel Network that does not have a `SubId`. + +The Base Asset can be returned anytime by calling the `base_asset_id()` function of the `AssetId` type. + +```sway +{{#include ../../../../examples/native_asset/src/main.sw:base_asset}} +``` + +## Basic Native Asset Functionality + +### Minting A Native Asset + +To mint a new asset, the `std::asset::mint()` function must be called internally within a contract. A `SubId` and amount of coins must be provided. These newly minted coins will be owned by the contract which minted them. To mint another asset from the same contract, replace the `DEFAULT_SUB_ID` with your desired `SubId`. + +```sway +{{#include ../../../../examples/native_asset/src/main.sw:mint_asset}} +``` + +You may also mint an asset to a specific entity with the `std::asset::mint_to()` function. Be sure to provide a target `Identity` that will own the newly minted coins. + +```sway +{{#include ../../../../examples/native_asset/src/main.sw:mint_to_asset}} +``` + +### Burning a Native Asset + +To burn an asset, the `std::asset::burn()` function must be called internally from the contract which minted them. The `SubId` used to mint the coins and amount must be provided. The burned coins must be owned by the contract. When an asset is burned it doesn't exist anymore. + +```sway +{{#include ../../../../examples/native_asset/src/main.sw:burn_asset}} +``` + +### Transfer a Native Asset + +To internally transfer a Native Asset, the `std::asset::transfer()` function must be called. A target `Identity` or user must be provided as well as the `AssetId` of the asset and an amount. + +```sway +{{#include ../../../../examples/native_asset/src/main.sw:transfer_asset}} +``` + +### Native Asset And Transactions -## Liquidity Pool Example +#### Getting The Transaction Asset -All contracts in Fuel can mint and burn their own native asset. Contracts can also receive and transfer any native asset including their own. Internal balances of all native assets pushed through calls or minted by the contract are tracked by the FuelVM and can be queried at any point using the balance_of function from the `std` library. Therefore, there is no need for any manual accounting of the contract's balances using persistent storage. +To query for the Native Asset sent in a transaction, you may call the `std::call_frames::msg_asset_id()` function. -The `std` library provides handy methods for accessing Fuel's native asset operations. +```sway +{{#include ../../../../examples/native_asset/src/main.sw:msg_asset_id}} +``` + +#### Getting The Transaction Amount + +To query for the amount of coins sent in a transaction, you may call the `std::context::msg_amount()` function. + +```sway +{{#include ../../../../examples/native_asset/src/main.sw:msg_amount}} +``` + +### Native Assets and Contracts -In this example, we show a basic liquidity pool contract minting its own native asset LP asset. +#### Checking A Contract's Balance + +To internally check a contract's balance, call the `std::context::this_balance()` function with the corresponding `AssetId`. + +```sway +{{#include ../../../../examples/native_asset/src/main.sw:this_balance}} +``` + +To check the balance of an external contract, call the `std::context::balance_of()` function with the corresponding `AssetId`. ```sway -{{#include ../../../../examples/liquidity_pool/src/main.sw}} +{{#include ../../../../examples/native_asset/src/main.sw:balance_of}} ``` -## Native Asset Example +> **NOTE** Due to the FuelVM's UTXO design, balances of `Address`'s cannot be returned in the Sway Language. This must be done off-chain using the SDK. + +#### Receiving Native Assets In A Contract -In this example, we show a native asset contract with more minting, burning and transferring capabilities. +By default, a contract may not receive a Native Asset in a contract call. To allow transferring of assets to the contract, add the `#[payable]` attribute to the function. ```sway -{{#include ../../../../examples/native_asset/src/main.sw}} +{{#include ../../../../examples/native_asset/src/main.sw:payable}} +``` + +## Native Asset Standards + +There are a number of standards developed to enable further functionality for Native Assets and help cross contract functionality. Information on standards can be found in the [Sway Standards Repo](https://github.com/FuelLabs/sway-standards). + +We currently have the following standards for Native Assets: + +- [SRC-20; Native Asset Standard](https://github.com/FuelLabs/sway-standards/blob/master/SRCs/src-20.md) defines the implementation of a standard API for Native Assets using the Sway Language. +- [SRC-3; Mint and Burn Standard](https://github.com/FuelLabs/sway-standards/blob/master/SRCs/src-3.md) is used to enable mint and burn functionality for Native Assets. +- [SRC-7; Arbitrary Asset Metadata Standard](https://github.com/FuelLabs/sway-standards/blob/master/SRCs/src-7.md) is used to store metadata for Native Assets. +- [SRC-6; Vault Standard](https://github.com/FuelLabs/sway-standards/blob/master/SRCs/src-6.md) defines the implementation of a standard API for asset vaults developed in Sway. + + +## Single Native Asset Example + +In this fully fleshed out example, we show a native asset contract which mints a single asset. This is the equivalent to the ERC-20 Standard use in Ethereum. Note there are no token approval functions. + +It implements the [SRC-20; Native Asset](https://github.com/FuelLabs/sway-standards/blob/master/SRCs/src-20.md), [SRC-3; Mint and Burn](https://github.com/FuelLabs/sway-standards/blob/master/SRCs/src-3.md), and [SRC-5; Ownership](https://github.com/FuelLabs/sway-standards/blob/master/SRCs/src-5.md) standards. + +```sway +// ERC20 equivalent in Sway. +contract; + +use src3::SRC3; +use src5::{SRC5, State, AccessError}; +use src20::SRC20; +use std::{ + asset::{ + burn, + mint_to, + }, + call_frames::{ + contract_id, + msg_asset_id, + }, + constants::DEFAULT_SUB_ID, + context::msg_amount, + string::String, +}; + +configurable { + DECIMALS: u8 = 9u8, + NAME: str[7] = __to_str_array("MyAsset"), + SYMBOL: str[5] = __to_str_array("MYTKN"), +} + +storage { + total_supply: u64 = 0, + owner: State = State::Uninitialized, +} + +// Native Asset Standard +impl SRC20 for Contract { + #[storage(read)] + fn total_assets() -> u64 { + 1 + } + + #[storage(read)] + fn total_supply(asset: AssetId) -> Option { + if asset == AssetId::default() { + Some(storage.total_supply.read()) + } else { + None + } + } + + #[storage(read)] + fn name(asset: AssetId) -> Option { + if asset == AssetId::default() { + Some(String::from_ascii_str(from_str_array(NAME))) + } else { + None + } + } + + #[storage(read)] + fn symbol(asset: AssetId) -> Option { + if asset == AssetId::default() { + Some(String::from_ascii_str(from_str_array(SYMBOL))) + } else { + None + } + } + + #[storage(read)] + fn decimals(asset: AssetId) -> Option { + if asset == AssetId::default() { + Some(DECIMALS) + } else { + None + } + } +} + +// Ownership Standard +impl SRC5 for Contract { + #[storage(read)] + fn owner() -> State { + storage.owner.read() + } +} + +// Mint and Burn Standard +impl SRC3 for Contract { + #[storage(read, write)] + fn mint(recipient: Identity, sub_id: SubId, amount: u64) { + require(sub_id == DEFAULT_SUB_ID, "incorrect-sub-id"); + require_access_owner(); + + storage + .total_supply + .write(amount + storage.total_supply.read()); + mint_to(recipient, DEFAULT_SUB_ID, amount); + } + + #[storage(read, write)] + fn burn(sub_id: SubId, amount: u64) { + require(sub_id == DEFAULT_SUB_ID, "incorrect-sub-id"); + require(msg_amount() >= amount, "incorrect-amount-provided"); + require( + msg_asset_id() == AssetId::default(), + "incorrect-asset-provided", + ); + require_access_owner(); + + storage + .total_supply + .write(storage.total_supply.read() - amount); + burn(DEFAULT_SUB_ID, amount); + } +} + +abi SingleAsset { + #[storage(read, write)] + fn constructor(owner_: Identity); +} + +impl SingleAsset for Contract { + #[storage(read, write)] + fn constructor(owner_: Identity) { + require(storage.owner.read() == State::Uninitialized, "owner-initialized"); + storage.owner.write(State::Initialized(owner_)); + } +} + +#[storage(read)] +fn require_access_owner() { + require( + storage.owner.read() == State::Initialized(msg_sender().unwrap()), + AccessError::NotOwner, + ); +} +``` + +## Multi Native Asset Example + +In this fully fleshed out example, we show a native asset contract which mints multiple assets. This is the equivalent to the ERC-1155 Standard use in Ethereum. Note there are no token approval functions. + +It implements the [SRC-20; Native Asset](https://github.com/FuelLabs/sway-standards/blob/master/SRCs/src-20.md), [SRC-3; Mint and Burn](https://github.com/FuelLabs/sway-standards/blob/master/SRCs/src-3.md), and [SRC-5; Ownership](https://github.com/FuelLabs/sway-standards/blob/master/SRCs/src-5.md) standards. + +```sway +// ERC1155 equivalent in Sway. +contract; + +use src5::{SRC5, State, AccessError}; +use src20::SRC20; +use src3::SRC3; +use std::{ + asset::{ + burn, + mint_to, + }, + call_frames::{ + contract_id, + msg_asset_id, + }, + hash::{ + Hash, + }, + context::this_balance, + storage::storage_string::*, + string::String +}; + +storage { + total_assets: u64 = 0, + total_supply: StorageMap = StorageMap {}, + name: StorageMap = StorageMap {}, + symbol: StorageMap = StorageMap {}, + decimals: StorageMap = StorageMap {}, + owner: State = State::Uninitialized, +} + +// Native Asset Standard +impl SRC20 for Contract { + #[storage(read)] + fn total_assets() -> u64 { + storage.total_assets.read() + } + + #[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() + } +} + +// Mint and Burn Standard +impl SRC3 for Contract { + #[storage(read, write)] + fn mint(recipient: Identity, sub_id: SubId, amount: u64) { + require_access_owner(); + let asset_id = AssetId::new(contract_id(), sub_id); + let supply = storage.total_supply.get(asset_id).try_read(); + if supply.is_none() { + storage.total_assets.write(storage.total_assets.try_read().unwrap_or(0) + 1); + } + let current_supply = supply.unwrap_or(0); + storage.total_supply.insert(asset_id, current_supply + amount); + mint_to(recipient, sub_id, amount); + } + + #[storage(read, write)] + fn burn(sub_id: SubId, amount: u64) { + require_access_owner(); + let asset_id = AssetId::new(contract_id(), sub_id); + require(this_balance(asset_id) >= amount, "not-enough-coins"); + + let supply = storage.total_supply.get(asset_id).try_read(); + let current_supply = supply.unwrap_or(0); + storage.total_supply.insert(asset_id, current_supply - amount); + burn(sub_id, amount); + } +} + +abi MultiAsset { + #[storage(read, write)] + fn constructor(owner_: Identity); + + #[storage(read, write)] + fn set_name(asset: AssetId, name: String); + + #[storage(read, write)] + fn set_symbol(asset: AssetId, symbol: String); + + #[storage(read, write)] + fn set_decimals(asset: AssetId, decimals: u8); +} + +impl MultiAsset for Contract { + #[storage(read, write)] + fn constructor(owner_: Identity) { + require(storage.owner.read() == State::Uninitialized, "owner-initialized"); + storage.owner.write(State::Initialized(owner_)); + } + + #[storage(read, write)] + fn set_name(asset: AssetId, name: String) { + require_access_owner(); + storage.name.insert(asset, StorageString {}); + storage.name.get(asset).write_slice(name); + } + + #[storage(read, write)] + fn set_symbol(asset: AssetId, symbol: String) { + require_access_owner(); + storage.symbol.insert(asset, StorageString {}); + storage.symbol.get(asset).write_slice(symbol); + } + + #[storage(read, write)] + fn set_decimals(asset: AssetId, decimals: u8) { + require_access_owner(); + storage.decimals.insert(asset, decimals); + } +} + +#[storage(read)] +fn require_access_owner() { + require( + storage.owner.read() == State::Initialized(msg_sender().unwrap()), + AccessError::NotOwner, + ); +} ``` diff --git a/docs/book/src/examples/liquidity_pool.md b/docs/book/src/examples/liquidity_pool.md new file mode 100644 index 00000000000..36dabba7f86 --- /dev/null +++ b/docs/book/src/examples/liquidity_pool.md @@ -0,0 +1,11 @@ +# Liquidity Pool Example + +All contracts in Fuel can mint and burn their own native asset. Contracts can also receive and transfer any native asset including their own. Internal balances of all native assets pushed through calls or minted by the contract are tracked by the FuelVM and can be queried at any point using the `balance_of` function from the `std` library. Therefore, there is no need for any manual accounting of the contract's balances using persistent storage. + +The `std` library provides handy methods for accessing Fuel's native asset operations. + +In this example, we show a basic liquidity pool contract minting its own native asset LP asset. + +```sway +{{#include ../../../../examples/liquidity_pool/src/main.sw}} +``` diff --git a/examples/native_asset/src/main.sw b/examples/native_asset/src/main.sw index d0645407173..8b7ca26bc44 100644 --- a/examples/native_asset/src/main.sw +++ b/examples/native_asset/src/main.sw @@ -1,56 +1,107 @@ contract; -use std::{asset::*, constants::DEFAULT_SUB_ID, context::*}; +use std::{asset::*, call_frames::msg_asset_id, constants::DEFAULT_SUB_ID, context::*}; abi NativeAsset { fn mint_coins(mint_amount: u64); fn burn_coins(burn_amount: u64); - fn force_transfer_coins(coins: u64, asset_id: AssetId, target: ContractId); - fn transfer_coins_to_output(coins: u64, asset_id: AssetId, recipient: Address); + fn transfer_coins(coins: u64, asset_id: AssetId, target: Identity); + #[payable] fn deposit(); fn get_balance(target: ContractId, asset_id: AssetId) -> u64; - fn mint_and_send_to_contract(amount: u64, destination: ContractId); - fn mint_and_send_to_address(amount: u64, recipient: Address); + fn get_msg_amount(); + fn this_balance(asset_id: AssetId) -> u64; + fn get_msg_asset_id(); + fn mint_coins_to(target_identity: Identity, mint_amount: u64); } impl NativeAsset for Contract { /// Mint an amount of this contracts native asset to the contracts balance. fn mint_coins(mint_amount: u64) { + // ANCHOR: mint_asset mint(DEFAULT_SUB_ID, mint_amount); + // ANCHOR_END: mint_asset + } + + fn mint_coins_to(target_identity: Identity, mint_amount: u64) { + // ANCHOR: mint_to_asset + mint_to(target_identity, DEFAULT_SUB_ID, mint_amount); + // ANCHOR_END: mint_to_asset } /// Burn an amount of this contracts native asset. fn burn_coins(burn_amount: u64) { + // ANCHOR: burn_asset burn(DEFAULT_SUB_ID, burn_amount); + // ANCHOR_END: burn_asset } /// Transfer coins to a target contract. - fn force_transfer_coins(coins: u64, asset_id: AssetId, target: ContractId) { - force_transfer_to_contract(target, asset_id, coins); + fn transfer_coins(coins: u64, asset_id: AssetId, target: Identity) { + // ANCHOR: transfer_asset + transfer(target, asset_id, coins); + // ANCHOR_END: transfer_asset } - /// Transfer coins to a transaction output to be spent later. - fn transfer_coins_to_output(coins: u64, asset_id: AssetId, recipient: Address) { - transfer_to_address(recipient, asset_id, coins); + /// Get the internal balance of a specific coin at a specific contract. + fn get_balance(target_contract: ContractId, asset_id: AssetId) -> u64 { + // ANCHOR: balance_of + balance_of(target_contract, asset_id) + // ANCHOR_END: balance_of } /// Get the internal balance of a specific coin at a specific contract. - fn get_balance(target: ContractId, asset_id: AssetId) -> u64 { - balance_of(target, asset_id) + fn this_balance(asset_id: AssetId) -> u64 { + // ANCHOR: this_balance + this_balance(asset_id) + // ANCHOR_END: this_balance } /// Deposit coins back into the contract. + // ANCHOR: payable + #[payable] fn deposit() { assert(msg_amount() > 0); } - + // ANCHOR_END: payable /// Mint and send this contracts native asset to a destination contract. - fn mint_and_send_to_contract(amount: u64, destination: ContractId) { - mint_to_contract(destination, DEFAULT_SUB_ID, amount); + fn get_msg_amount() { + // ANCHOR: msg_amount + let amount = msg_amount(); + // ANCHOR_END: msg_amount } - /// Mind and send this contracts native asset to a destination address. - fn mint_and_send_to_address(amount: u64, recipient: Address) { - mint_to_address(recipient, DEFAULT_SUB_ID, amount); + /// Mint and send this contracts native asset to a destination contract. + fn get_msg_asset_id() { + // ANCHOR: msg_asset_id + let amount = msg_asset_id(); + // ANCHOR_END: msg_asset_id } } + +fn get_base_asset() { + // ANCHOR: base_asset + let base_asset: AssetId = AssetId::base_asset_id(); + // ANCHOR_END: base_asset +} + +fn default_asset_id() { + // ANCHOR: default_asset_id + let asset_id: AssetId = AssetId::default(); + // ANCHOR_END: default_asset_id +} + +fn new_asset_id(my_contract_id: ContractId, my_sub_id: SubId) { + // ANCHOR: new_asset_id + let my_contract_id: ContractId = ContractId::from(0x1000000000000000000000000000000000000000000000000000000000000000); + let my_sub_id: SubId = 0x2000000000000000000000000000000000000000000000000000000000000000; + + let asset_id: AssetId = AssetId::new(my_contract_id, my_sub_id); + // ANCHOR_END: new_asset_id +} + +fn from_asset_id() { + // ANCHOR: from_asset_id + let asset_id: AssetId = AssetId::from(0x0000000000000000000000000000000000000000000000000000000000000000); + // ANCHOR_END: from_asset_id +}