Unverified Commit a314b349 authored by Steve Degosserie's avatar Steve Degosserie Committed by GitHub
Browse files

PSP22 Chain Extension Example (#1244)



* PSP22 chain extension example

* Format code

* Format code

* Format code

* Rename example folder

* Code & README cleanup

* ink version bump and reformat

* Split call function into smaller functions

* Added some comments

* Implemented decrease_allowance

* Removed unnecessary local package versions

* Apply suggestions from code review
Co-authored-by: default avatarHernando Castano <HCastano@users.noreply.github.com>

* Resolve issues mentioned in review

* Amend to the latest changes in chain extensions

* Reformat

* Adjust to review comments.

* Move `Ok` out of match.
Co-authored-by: default avatarHernando Castano <HCastano@users.noreply.github.com>

* Comments & formatting

* Fix up comment style
Co-authored-by: Adam Wierzbicki's avatarAdam Wierzbicki <adam.wierzbicki@parity.io>
Co-authored-by: default avatarHernando Castano <HCastano@users.noreply.github.com>
parent 2b7f20e8
Pipeline #214068 passed with stages
in 19 minutes and 10 seconds
......@@ -128,6 +128,8 @@ examples-fmt:
cargo +nightly fmt --verbose --manifest-path ./examples/upgradeable-contracts/${contract}/Cargo.toml -- --check;
done
- cargo +nightly fmt --verbose --manifest-path ./examples/upgradeable-contracts/set-code-hash/updated-incrementer/Cargo.toml -- --check
# This file is not a part of the cargo project, so it wouldn't be formatted the usual way
- rustfmt +nightly --verbose --check ./examples/psp22-extension/runtime/psp22-extension-example.rs
allow_failure: true
clippy-std:
......
# Ignore build artifacts from the local tests sub-crate.
/target/
# Ignore backup files creates by cargo fmt.
**/*.rs.bk
# Remove Cargo.lock when creating an executable, leave it for libraries
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
Cargo.lock
[package]
name = "psp22_extension"
version = "4.0.0-alpha.1"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2021"
publish = false
[dependencies]
ink_prelude = { path = "../../crates/prelude", default-features = false }
ink_primitives = { path = "../../crates/primitives", default-features = false }
ink_metadata = { path = "../../crates/metadata", default-features = false, features = ["derive"], optional = true }
ink_env = { path = "../../crates/env", default-features = false }
ink_storage = { path = "../../crates/storage", default-features = false }
ink_lang = { path = "../../crates/lang", default-features = false }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }
[lib]
name = "psp22_extension"
path = "lib.rs"
crate-type = ["cdylib"]
[features]
default = ["std"]
std = [
"ink_metadata/std",
"ink_env/std",
"ink_storage/std",
"ink_prelude/std",
"ink_primitives/std",
"scale/std",
"scale-info/std",
]
ink-as-dependency = []
# PSP22 Chain Extension Example
## What is this example about?
It is an example implementation of the
[PSP22 Fungible Token Standard](https://github.com/w3f/PSPs/blob/master/PSPs/psp-22.md)
as a chain extension, supporting a multi-token system provided by the
[FRAME assets pallet](https://docs.substrate.io/rustdocs/latest/pallet_assets/index.html).
It effectively allows ink! contracts (L2) to interact with native assets (L1) from the
chain runtime in a standardized way.
See [this chapter](https://paritytech.github.io/ink-docs/macros-attributes/chain-extension)
in our ink! documentation for more details about chain extensions.
There are two parts to this example:
* Defining and calling the extension in ink!.
* Defining the extension in Substrate.
## Chain-side Integration
To integrate this example into Substrate you need to do two things:
* In your runtime, use the code in
[`psp22-extension-example.rs`](runtime/psp22-extension-example.rs)
as an implementation for the trait `ChainExtension` in Substrate.
You can just copy/paste that file as a new module, e.g. `runtime/src/chain_extension.rs`.
* In your runtime, use the implementation as the associated type `ChainExtension` of the
trait `pallet_contracts::Config`:
```rust
impl pallet_contracts::Config for Runtime {
type ChainExtension = Psp22Extension;
}
```
## ink! Integration
See the example contract in [`lib.rs`](lib.rs).
## Disclaimer
:warning: This is not a feature-complete or production-ready PSP22 implementation. This
example currently lacks proper error management, precise weight accounting, tests (these
all might be added at a later point).
#![cfg_attr(not(feature = "std"), no_std)]
use ink_env::Environment;
use ink_lang as ink;
use ink_prelude::vec::Vec;
type DefaultAccountId = <ink_env::DefaultEnvironment as Environment>::AccountId;
type DefaultBalance = <ink_env::DefaultEnvironment as Environment>::Balance;
#[ink::chain_extension]
pub trait Psp22Extension {
type ErrorCode = Psp22Error;
// PSP22 Metadata interfaces
#[ink(extension = 0x3d26)]
fn token_name(asset_id: u32) -> Result<Vec<u8>>;
#[ink(extension = 0x3420)]
fn token_symbol(asset_id: u32) -> Result<Vec<u8>>;
#[ink(extension = 0x7271)]
fn token_decimals(asset_id: u32) -> Result<u8>;
// PSP22 interface queries
#[ink(extension = 0x162d)]
fn total_supply(asset_id: u32) -> Result<DefaultBalance>;
#[ink(extension = 0x6568)]
fn balance_of(asset_id: u32, owner: DefaultAccountId) -> Result<DefaultBalance>;
#[ink(extension = 0x4d47)]
fn allowance(
asset_id: u32,
owner: DefaultAccountId,
spender: DefaultAccountId,
) -> Result<DefaultBalance>;
// PSP22 transfer
#[ink(extension = 0xdb20)]
fn transfer(asset_id: u32, to: DefaultAccountId, value: DefaultBalance)
-> Result<()>;
// PSP22 transfer_from
#[ink(extension = 0x54b3)]
fn transfer_from(
asset_id: u32,
from: DefaultAccountId,
to: DefaultAccountId,
value: DefaultBalance,
) -> Result<()>;
// PSP22 approve
#[ink(extension = 0xb20f)]
fn approve(
asset_id: u32,
spender: DefaultAccountId,
value: DefaultBalance,
) -> Result<()>;
// PSP22 increase_allowance
#[ink(extension = 0x96d6)]
fn increase_allowance(
asset_id: u32,
spender: DefaultAccountId,
value: DefaultBalance,
) -> Result<()>;
// PSP22 decrease_allowance
#[ink(extension = 0xfecb)]
fn decrease_allowance(
asset_id: u32,
spender: DefaultAccountId,
value: DefaultBalance,
) -> Result<()>;
}
#[derive(scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Psp22Error {
TotalSupplyFailed,
}
pub type Result<T> = core::result::Result<T, Psp22Error>;
impl From<scale::Error> for Psp22Error {
fn from(_: scale::Error) -> Self {
panic!("encountered unexpected invalid SCALE encoding")
}
}
impl ink_env::chain_extension::FromStatusCode for Psp22Error {
fn from_status_code(status_code: u32) -> core::result::Result<(), Self> {
match status_code {
0 => Ok(()),
1 => Err(Self::TotalSupplyFailed),
_ => panic!("encountered unknown status code"),
}
}
}
/// An environment using default ink environment types, with PSP-22 extension included
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum CustomEnvironment {}
impl Environment for CustomEnvironment {
const MAX_EVENT_TOPICS: usize =
<ink_env::DefaultEnvironment as Environment>::MAX_EVENT_TOPICS;
type AccountId = DefaultAccountId;
type Balance = DefaultBalance;
type Hash = <ink_env::DefaultEnvironment as Environment>::Hash;
type Timestamp = <ink_env::DefaultEnvironment as Environment>::Timestamp;
type BlockNumber = <ink_env::DefaultEnvironment as Environment>::BlockNumber;
type ChainExtension = crate::Psp22Extension;
}
#[ink::contract(env = crate::CustomEnvironment)]
mod psp22_ext {
use super::{
Result,
Vec,
};
/// A chain extension which implements the PSP-22 fungible token standard.
/// For more details see <https://github.com/w3f/PSPs/blob/master/PSPs/psp-22.md>
#[ink(storage)]
#[derive(Default)]
pub struct Psp22Extension {}
impl Psp22Extension {
/// Creates a new instance of this contract.
#[ink(constructor)]
pub fn new() -> Self {
Default::default()
}
// PSP22 Metadata interfaces
/// Returns the token name of the specified asset.
#[ink(message, selector = 0x3d261bd4)]
pub fn token_name(&self, asset_id: u32) -> Result<Vec<u8>> {
self.env().extension().token_name(asset_id)
}
/// Returns the token symbol of the specified asset.
#[ink(message, selector = 0x34205be5)]
pub fn token_symbol(&self, asset_id: u32) -> Result<Vec<u8>> {
self.env().extension().token_symbol(asset_id)
}
/// Returns the token decimals of the specified asset.
#[ink(message, selector = 0x7271b782)]
pub fn token_decimals(&self, asset_id: u32) -> Result<u8> {
self.env().extension().token_decimals(asset_id)
}
// PSP22 interface queries
/// Returns the total token supply of the specified asset.
#[ink(message, selector = 0x162df8c2)]
pub fn total_supply(&self, asset_id: u32) -> Result<Balance> {
self.env().extension().total_supply(asset_id)
}
/// Returns the account balance for the specified asset & owner.
#[ink(message, selector = 0x6568382f)]
pub fn balance_of(&self, asset_id: u32, owner: AccountId) -> Result<Balance> {
self.env().extension().balance_of(asset_id, owner)
}
/// Returns the amount which `spender` is still allowed to withdraw from `owner`
/// for the specified asset.
#[ink(message, selector = 0x4d47d921)]
pub fn allowance(
&self,
asset_id: u32,
owner: AccountId,
spender: AccountId,
) -> Result<Balance> {
self.env().extension().allowance(asset_id, owner, spender)
}
// PSP22 transfer
/// Transfers `value` amount of specified asset from the caller's account to the
/// account `to`.
#[ink(message, selector = 0xdb20f9f5)]
pub fn transfer(
&mut self,
asset_id: u32,
to: AccountId,
value: Balance,
) -> Result<()> {
self.env().extension().transfer(asset_id, to, value)
}
// PSP22 transfer_from
/// Transfers `value` amount of specified asset on the behalf of `from` to the
/// account `to`.
#[ink(message, selector = 0x54b3c76e)]
pub fn transfer_from(
&mut self,
asset_id: u32,
from: AccountId,
to: AccountId,
value: Balance,
) -> Result<()> {
self.env()
.extension()
.transfer_from(asset_id, from, to, value)
}
// PSP22 approve
/// Allows `spender` to withdraw from the caller's account multiple times, up to
/// the `value` amount of the specified asset.
#[ink(message, selector = 0xb20f1bbd)]
pub fn approve(
&mut self,
asset_id: u32,
spender: AccountId,
value: Balance,
) -> Result<()> {
self.env().extension().approve(asset_id, spender, value)
}
// PSP22 increase_allowance
/// Atomically increases the allowance for the specified asset granted to `spender`
/// by the caller.
#[ink(message, selector = 0x96d6b57a)]
pub fn increase_allowance(
&mut self,
asset_id: u32,
spender: AccountId,
value: Balance,
) -> Result<()> {
self.env()
.extension()
.increase_allowance(asset_id, spender, value)
}
// PSP22 decrease_allowance
/// Atomically decreases the allowance for the specified asset granted to `spender`
/// by the caller.
#[ink(message, selector = 0xfecb57d5)]
pub fn decrease_allowance(
&mut self,
asset_id: u32,
spender: AccountId,
value: Balance,
) -> Result<()> {
self.env()
.extension()
.decrease_allowance(asset_id, spender, value)
}
}
}
use codec::{
Decode,
Encode,
MaxEncodedLen,
};
use frame_support::{
dispatch::RawOrigin,
log::{
error,
trace,
},
pallet_prelude::*,
traits::fungibles::{
approvals::{
Inspect as AllowanceInspect,
Mutate as AllowanceMutate,
},
Inspect,
InspectMetadata,
Transfer,
},
};
use pallet_assets::{
self,
WeightInfo,
};
use pallet_contracts::chain_extension::{
ChainExtension,
Environment,
Ext,
InitState,
RetVal,
SysConfig,
UncheckedFrom,
};
use sp_runtime::{
traits::{
Saturating,
StaticLookup,
Zero,
},
DispatchError,
};
#[derive(Debug, PartialEq, Encode, Decode, MaxEncodedLen)]
struct Psp22BalanceOfInput<AssetId, AccountId> {
asset_id: AssetId,
owner: AccountId,
}
#[derive(Debug, PartialEq, Encode, Decode, MaxEncodedLen)]
struct Psp22AllowanceInput<AssetId, AccountId> {
asset_id: AssetId,
owner: AccountId,
spender: AccountId,
}
#[derive(Debug, PartialEq, Encode, Decode, MaxEncodedLen)]
struct Psp22TransferInput<AssetId, AccountId, Balance> {
asset_id: AssetId,
to: AccountId,
value: Balance,
}
#[derive(Debug, PartialEq, Encode, Decode, MaxEncodedLen)]
struct Psp22TransferFromInput<AssetId, AccountId, Balance> {
asset_id: AssetId,
from: AccountId,
to: AccountId,
value: Balance,
}
#[derive(Debug, PartialEq, Encode, Decode, MaxEncodedLen)]
struct Psp22ApproveInput<AssetId, AccountId, Balance> {
asset_id: AssetId,
spender: AccountId,
value: Balance,
}
#[derive(Default)]
pub struct Psp22Extension;
fn convert_err(err_msg: &'static str) -> impl FnOnce(DispatchError) -> DispatchError {
move |err| {
trace!(
target: "runtime",
"PSP22 Transfer failed:{:?}",
err
);
DispatchError::Other(err_msg)
}
}
/// We're using enums for function IDs because contrary to raw u16 it enables
/// exhaustive matching, which results in cleaner code.
enum FuncId {
Metadata(Metadata),
Query(Query),
Transfer,
TransferFrom,
Approve,
IncreaseAllowance,
DecreaseAllowance,
}
#[derive(Debug)]
enum Metadata {
Name,
Symbol,
Decimals,
}
#[derive(Debug)]
enum Query {
TotalSupply,
BalanceOf,
Allowance,
}
impl TryFrom<u16> for FuncId {
type Error = DispatchError;
fn try_from(func_id: u16) -> Result<Self, Self::Error> {
let id = match func_id {
// Note: We use the first two bytes of PSP22 interface selectors as function IDs,
// While we can use anything here, it makes sense from a convention perspective.
0x3d26 => Self::Metadata(Metadata::Name),
0x3420 => Self::Metadata(Metadata::Symbol),
0x7271 => Self::Metadata(Metadata::Decimals),
0x162d => Self::Query(Query::TotalSupply),
0x6568 => Self::Query(Query::BalanceOf),
0x4d47 => Self::Query(Query::Allowance),
0xdb20 => Self::Transfer,
0x54b3 => Self::TransferFrom,
0xb20f => Self::Approve,
0x96d6 => Self::IncreaseAllowance,
0xfecb => Self::DecreaseAllowance,
_ => {
error!("Called an unregistered `func_id`: {:}", func_id);
return Err(DispatchError::Other("Unimplemented func_id"))
}
};
Ok(id)
}
}
fn metadata<T, E>(
func_id: Metadata,
env: Environment<E, InitState>,
) -> Result<(), DispatchError>
where
T: pallet_assets::Config + pallet_contracts::Config,
<T as SysConfig>::AccountId: UncheckedFrom<<T as SysConfig>::Hash> + AsRef<[u8]>,
E: Ext<T = T>,
{
let mut env = env.buf_in_buf_out();
let asset_id = env.read_as()?;
let result = match func_id {
Metadata::Name => {
<pallet_assets::Pallet<T> as InspectMetadata<T::AccountId>>::name(&asset_id)
.encode()
}
Metadata::Symbol => {
<pallet_assets::Pallet<T> as InspectMetadata<T::AccountId>>::symbol(&asset_id)
.encode()
}
Metadata::Decimals => {
<pallet_assets::Pallet<T> as InspectMetadata<T::AccountId>>::decimals(
&asset_id,
)
.encode()
}
};
trace!(
target: "runtime",
"[ChainExtension] PSP22Metadata::{:?}",
func_id
);
env.write(&result, false, None)
.map_err(convert_err("ChainExtension failed to call PSP22Metadata"))
}
fn query<T, E>(
func_id: Query,
env: Environment<E, InitState>,
) -> Result<(), DispatchError>
where
T: pallet_assets::Config + pallet_contracts::Config,
<T as SysConfig>::AccountId: UncheckedFrom<<T as SysConfig>::Hash> + AsRef<[u8]>,
E: Ext<T = T>,
{
let mut env = env.buf_in_buf_out();
let result = match func_id {
Query::TotalSupply => {
let asset_id = env.read_as()?;
<pallet_assets::Pallet<T> as Inspect<T::AccountId>>::total_issuance(asset_id)
}
Query::BalanceOf => {
let input: Psp22BalanceOfInput<T::AssetId, T::AccountId> = env.read_as()?;
<pallet_assets::Pallet<T> as Inspect<T::AccountId>>::balance(
input.asset_id,
&input.owner,
)
}
Query::Allowance => {
let input: Psp22AllowanceInput<T::AssetId, T::AccountId> = env.read_as()?;
<pallet_assets::Pallet<T> as AllowanceInspect<T::AccountId>>::allowance(
input.asset_id,
&input.owner,
&input.spender,
)
}
}
.encode();
trace!(
target: "runtime",
"[ChainExtension] PSP22::{:?}",
func_id
);
env.write(&result, false, None)
.map_err(convert_err("ChainExtension failed to call PSP22 query"))
}
fn transfer<T, E>(env: Environment<E, InitState>) -> Result<(), DispatchError>
where
T: pallet_assets::Config + pallet_contracts::Config,
<T as SysConfig>::AccountId: UncheckedFrom<<T as SysConfig>::Hash> + AsRef<[u8]>,
E: Ext<T = T>,
{
let mut env = env.buf_in_buf_out();