From a314b3499a510aed08d24703769f72130220b222 Mon Sep 17 00:00:00 2001 From: Steve Degosserie Date: Thu, 8 Sep 2022 20:24:21 +0200 Subject: [PATCH] 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: Hernando Castano * 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: Hernando Castano * Comments & formatting * Fix up comment style Co-authored-by: Adam Wierzbicki Co-authored-by: Hernando Castano --- .gitlab-ci.yml | 2 + examples/psp22-extension/.gitignore | 9 + examples/psp22-extension/Cargo.toml | 35 ++ examples/psp22-extension/README.md | 47 ++ examples/psp22-extension/lib.rs | 264 +++++++++++ .../runtime/psp22-extension-example.rs | 445 ++++++++++++++++++ 6 files changed, 802 insertions(+) create mode 100755 examples/psp22-extension/.gitignore create mode 100755 examples/psp22-extension/Cargo.toml create mode 100644 examples/psp22-extension/README.md create mode 100755 examples/psp22-extension/lib.rs create mode 100644 examples/psp22-extension/runtime/psp22-extension-example.rs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a9471e6904..771cad513c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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: diff --git a/examples/psp22-extension/.gitignore b/examples/psp22-extension/.gitignore new file mode 100755 index 0000000000..8de8f877e4 --- /dev/null +++ b/examples/psp22-extension/.gitignore @@ -0,0 +1,9 @@ +# 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 diff --git a/examples/psp22-extension/Cargo.toml b/examples/psp22-extension/Cargo.toml new file mode 100755 index 0000000000..92e34a0705 --- /dev/null +++ b/examples/psp22-extension/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "psp22_extension" +version = "4.0.0-alpha.1" +authors = ["Parity Technologies "] +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 = [] diff --git a/examples/psp22-extension/README.md b/examples/psp22-extension/README.md new file mode 100644 index 0000000000..040662f563 --- /dev/null +++ b/examples/psp22-extension/README.md @@ -0,0 +1,47 @@ +# 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). diff --git a/examples/psp22-extension/lib.rs b/examples/psp22-extension/lib.rs new file mode 100755 index 0000000000..d8ed8ae850 --- /dev/null +++ b/examples/psp22-extension/lib.rs @@ -0,0 +1,264 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use ink_env::Environment; +use ink_lang as ink; +use ink_prelude::vec::Vec; + +type DefaultAccountId = ::AccountId; +type DefaultBalance = ::Balance; + +#[ink::chain_extension] +pub trait Psp22Extension { + type ErrorCode = Psp22Error; + + // PSP22 Metadata interfaces + + #[ink(extension = 0x3d26)] + fn token_name(asset_id: u32) -> Result>; + + #[ink(extension = 0x3420)] + fn token_symbol(asset_id: u32) -> Result>; + + #[ink(extension = 0x7271)] + fn token_decimals(asset_id: u32) -> Result; + + // PSP22 interface queries + + #[ink(extension = 0x162d)] + fn total_supply(asset_id: u32) -> Result; + + #[ink(extension = 0x6568)] + fn balance_of(asset_id: u32, owner: DefaultAccountId) -> Result; + + #[ink(extension = 0x4d47)] + fn allowance( + asset_id: u32, + owner: DefaultAccountId, + spender: DefaultAccountId, + ) -> Result; + + // 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 = core::result::Result; + +impl From 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 = + ::MAX_EVENT_TOPICS; + + type AccountId = DefaultAccountId; + type Balance = DefaultBalance; + type Hash = ::Hash; + type Timestamp = ::Timestamp; + type BlockNumber = ::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 + #[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> { + 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> { + 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 { + 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 { + 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 { + 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 { + 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) + } + } +} diff --git a/examples/psp22-extension/runtime/psp22-extension-example.rs b/examples/psp22-extension/runtime/psp22-extension-example.rs new file mode 100644 index 0000000000..922cb82afe --- /dev/null +++ b/examples/psp22-extension/runtime/psp22-extension-example.rs @@ -0,0 +1,445 @@ +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 { + asset_id: AssetId, + owner: AccountId, +} + +#[derive(Debug, PartialEq, Encode, Decode, MaxEncodedLen)] +struct Psp22AllowanceInput { + asset_id: AssetId, + owner: AccountId, + spender: AccountId, +} + +#[derive(Debug, PartialEq, Encode, Decode, MaxEncodedLen)] +struct Psp22TransferInput { + asset_id: AssetId, + to: AccountId, + value: Balance, +} + +#[derive(Debug, PartialEq, Encode, Decode, MaxEncodedLen)] +struct Psp22TransferFromInput { + asset_id: AssetId, + from: AccountId, + to: AccountId, + value: Balance, +} + +#[derive(Debug, PartialEq, Encode, Decode, MaxEncodedLen)] +struct Psp22ApproveInput { + 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 for FuncId { + type Error = DispatchError; + + fn try_from(func_id: u16) -> Result { + 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( + func_id: Metadata, + env: Environment, +) -> Result<(), DispatchError> +where + T: pallet_assets::Config + pallet_contracts::Config, + ::AccountId: UncheckedFrom<::Hash> + AsRef<[u8]>, + E: Ext, +{ + let mut env = env.buf_in_buf_out(); + let asset_id = env.read_as()?; + let result = match func_id { + Metadata::Name => { + as InspectMetadata>::name(&asset_id) + .encode() + } + Metadata::Symbol => { + as InspectMetadata>::symbol(&asset_id) + .encode() + } + Metadata::Decimals => { + as InspectMetadata>::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( + func_id: Query, + env: Environment, +) -> Result<(), DispatchError> +where + T: pallet_assets::Config + pallet_contracts::Config, + ::AccountId: UncheckedFrom<::Hash> + AsRef<[u8]>, + E: Ext, +{ + let mut env = env.buf_in_buf_out(); + let result = match func_id { + Query::TotalSupply => { + let asset_id = env.read_as()?; + as Inspect>::total_issuance(asset_id) + } + Query::BalanceOf => { + let input: Psp22BalanceOfInput = env.read_as()?; + as Inspect>::balance( + input.asset_id, + &input.owner, + ) + } + Query::Allowance => { + let input: Psp22AllowanceInput = env.read_as()?; + as AllowanceInspect>::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(env: Environment) -> Result<(), DispatchError> +where + T: pallet_assets::Config + pallet_contracts::Config, + ::AccountId: UncheckedFrom<::Hash> + AsRef<[u8]>, + E: Ext, +{ + let mut env = env.buf_in_buf_out(); + let base_weight = ::WeightInfo::transfer(); + // debug_message weight is a good approximation of the additional overhead of going from + // contract layer to substrate layer. + let overhead = Weight::from_ref_time( + ::Schedule::get() + .host_fn_weights + .debug_message, + ); + let charged_weight = env.charge_weight(base_weight.saturating_add(overhead))?; + trace!( + target: "runtime", + "[ChainExtension]|call|transfer / charge_weight:{:?}", + charged_weight + ); + + let input: Psp22TransferInput = + env.read_as()?; + let sender = env.ext().caller(); + + as Transfer>::transfer( + input.asset_id, + sender, + &input.to, + input.value, + true, + ) + .map_err(convert_err("ChainExtension failed to call transfer"))?; + trace!( + target: "runtime", + "[ChainExtension]|call|transfer" + ); + + Ok(()) +} + +fn transfer_from(env: Environment) -> Result<(), DispatchError> +where + T: pallet_assets::Config + pallet_contracts::Config, + ::AccountId: UncheckedFrom<::Hash> + AsRef<[u8]>, + E: Ext, +{ + let mut env = env.buf_in_buf_out(); + let base_weight = ::WeightInfo::transfer(); + // debug_message weight is a good approximation of the additional overhead of going from + // contract layer to substrate layer. + let overhead = Weight::from_ref_time( + ::Schedule::get() + .host_fn_weights + .debug_message, + ); + let charged_amount = env.charge_weight(base_weight.saturating_add(overhead))?; + trace!( + target: "runtime", + "[ChainExtension]|call|transfer / charge_weight:{:?}", + charged_amount + ); + + let input: Psp22TransferFromInput = + env.read_as()?; + let spender = env.ext().caller(); + + let result = + as AllowanceMutate>::transfer_from( + input.asset_id, + &input.from, + spender, + &input.to, + input.value, + ); + trace!( + target: "runtime", + "[ChainExtension]|call|transfer_from" + ); + result.map_err(convert_err("ChainExtension failed to call transfer_from")) +} + +fn approve(env: Environment) -> Result<(), DispatchError> +where + T: pallet_assets::Config + pallet_contracts::Config, + ::AccountId: UncheckedFrom<::Hash> + AsRef<[u8]>, + E: Ext, +{ + let mut env = env.buf_in_buf_out(); + let base_weight = ::WeightInfo::approve_transfer(); + // debug_message weight is a good approximation of the additional overhead of going from + // contract layer to substrate layer. + let overhead = Weight::from_ref_time( + ::Schedule::get() + .host_fn_weights + .debug_message, + ); + let charged_weight = env.charge_weight(base_weight.saturating_add(overhead))?; + trace!( + target: "runtime", + "[ChainExtension]|call|approve / charge_weight:{:?}", + charged_weight + ); + + let input: Psp22ApproveInput = env.read_as()?; + let owner = env.ext().caller(); + + let result = as AllowanceMutate>::approve( + input.asset_id, + owner, + &input.spender, + input.value, + ); + trace!( + target: "runtime", + "[ChainExtension]|call|approve" + ); + result.map_err(convert_err("ChainExtension failed to call approve")) +} + +fn decrease_allowance(env: Environment) -> Result<(), DispatchError> +where + T: pallet_assets::Config + pallet_contracts::Config, + ::AccountId: UncheckedFrom<::Hash> + AsRef<[u8]>, + E: Ext, +{ + let mut env = env.buf_in_buf_out(); + let input: Psp22ApproveInput = env.read_as()?; + if input.value.is_zero() { + return Ok(()) + } + + let base_weight = ::WeightInfo::cancel_approval() + .saturating_add(::WeightInfo::approve_transfer()); + // debug_message weight is a good approximation of the additional overhead of going from + // contract layer to substrate layer. + let overhead = Weight::from_ref_time( + ::Schedule::get() + .host_fn_weights + .debug_message, + ); + let charged_weight = env.charge_weight(base_weight.saturating_add(overhead))?; + trace!( + target: "runtime", + "[ChainExtension]|call|decrease_allowance / charge_weight:{:?}", + charged_weight + ); + + let owner = env.ext().caller(); + let mut allowance = + as AllowanceInspect>::allowance( + input.asset_id, + owner, + &input.spender, + ); + >::cancel_approval( + RawOrigin::Signed(owner.clone()).into(), + input.asset_id, + T::Lookup::unlookup(input.spender.clone()), + ) + .map_err(convert_err( + "ChainExtension failed to call decrease_allowance", + ))?; + allowance.saturating_reduce(input.value); + if allowance.is_zero() { + // If reduce value was less or equal than existing allowance, it should stay none. + env.adjust_weight( + charged_weight, + ::WeightInfo::cancel_approval() + .saturating_add(overhead), + ); + return Ok(()) + } + as AllowanceMutate>::approve( + input.asset_id, + owner, + &input.spender, + allowance, + ) + .map_err(convert_err( + "ChainExtension failed to call decrease_allowance", + ))?; + + trace!( + target: "runtime", + "[ChainExtension]|call|decrease_allowance" + ); + + Ok(()) +} + +impl ChainExtension for Psp22Extension +where + T: pallet_assets::Config + pallet_contracts::Config, + ::AccountId: UncheckedFrom<::Hash> + AsRef<[u8]>, +{ + fn call( + &mut self, + env: Environment, + ) -> Result + where + E: Ext, + ::AccountId: + UncheckedFrom<::Hash> + AsRef<[u8]>, + { + let func_id = FuncId::try_from(env.func_id())?; + match func_id { + FuncId::Metadata(func_id) => metadata::(func_id, env)?, + FuncId::Query(func_id) => query::(func_id, env)?, + FuncId::Transfer => transfer::(env)?, + FuncId::TransferFrom => transfer_from::(env)?, + // This is a bit of a shortcut. It was made because the documentation + // for Mutate::approve does not specify the result of subsequent calls. + FuncId::Approve | FuncId::IncreaseAllowance => approve::(env)?, + FuncId::DecreaseAllowance => decrease_allowance(env)?, + } + + Ok(RetVal::Converging(0)) + } +}