Unverified Commit afc48714 authored by Hernando Castano's avatar Hernando Castano Committed by GitHub
Browse files

ERC-1155 Example (#800)



* Add basic contract skeleton

* Add dummy ERC-1155 trait implementations

* Implement `balance_of` method

* First attempt at `balance_of_batch` implementation

I'm not sure if the output format is correct, need to read the docs
more closely

* Implement simple token transfer

* Flatten balances BTreeMap

* Clean up account usage in tests

* Implement approval mechanism

* Fix bug when sending tokens to an account with zero balance

* Check approvals before sending tokens

* Suppress warnings

* Appease Clippy

* Add crude support for token transfers to smart contracts

* Simplify check for smart contract-ness

* Handle receiving tokens as a smart contract

* Implement `safe_transfer_from` method

* Only do approval and recipient checks during in batch transfers

* I was wrong about the compiler's cleverness...

* Add documentation about interface

* Make better use of some imports

* Disallow owners from approving themselves for token tranfers

* Allow creating and minting tokens

* Derive default for storage struct

* Add note on on-chain panic

* Remove `with_balances` constructor

It wasn't ERC-1155 compliant (no transfer events emitted) and it
also leaked the internal structure of how balances were tracked.

* RustFmt with Nightly

Not sure I like some of the decisions though...

* Tag on_received messages with selectors

* Add missing event

* Index topics in events

* Remove note on BTreeSet usage

Can't figure out how to get tests to compile with it.

* Stop panicking on cross-contract call error

However, this is only because I have no feedback on why this call
is actually failing. This behaviour should be added back.

* Nightly RustFmt

* Fix RustDoc links

* Remove inline questions

* Remove unused `data` argument from `create/mint`

* Rename magic value contants

* Remove data argument from `mint/create` tests

* Use entry API when decreasing account balance

* Extract approvals pairs into struct

This is better in terms of type safety and ease of use

* Improve some of the panic messages

* Cache calls to `self.env().caller()`

* Allow `TransferSingle` events to contain Optional addresses

This slightly deviates from the spec which says we should use the `0x00`
address during certain operations, but this is more idiomatic Rust.

* Add logging around calls to `onERC1155Received`

* Improve debug message when receiving cross-contract results

* Move warning lints to specific lines of code

* Format code

* Remove backticks from URLs
Co-authored-by: Michael Müller's avatarMichael Müller <michi@parity.io>

* Fix comment wording/typo

* Add expected panic messages to tests

* Move imports related to x-contract calls closer to use site

* Change selector bytes to hex for the humans

* Remove incorrect comment about off-chain environment testing

* Add documentation for `TokenId`

This will make sure that it doesn't show up as `u128` in
the generated docs.

* Nightly RustFmt

* Uppercase selector bytes

* Don't repeat `erc_1155` in `Erc1155TokenReceiver` methods

* Nightly RustFmt

* Appease the spellchecker

* Use Environment typedef

* Allow tests to run in stable and experimental off-chain envs

* Add explanation as to why we don't accept tokens

* Return `Result` when minting tokens

* Allow (most) errors to be handled gracefully by caller

* Nightly RustFmt

* Add shorthand zero-address to allowed spelling list

* Run tests with `--features ink-experimental-engine` in CI

* Perform batch balance checks before trying to transfer tokens

* Move smart contract transfer checks to their own helper function

* Appease Clippy

* Make `ensure` macro definition more explicit
Co-authored-by: default avatarRobin Freyler <robin.freyler@gmail.com>

* Iterate over values instead of references
Co-authored-by: default avatarRobin Freyler <robin.freyler@gmail.com>

* Iterate over references again

* Return a value from `on_batch_received`

* Don't collect into intermediate Vec

* Wrap 0x00 in code blocks

This way the spellchecker will ignore it and we
can avoid adding it to our dictionary.
Co-authored-by: Michael Müller's avatarMichael Müller <michi@parity.io>
Co-authored-by: default avatarRobin Freyler <robin.freyler@gmail.com>
parent 4e2c7c30
Pipeline #146261 canceled with stages
in 22 minutes and 25 seconds
......@@ -12,6 +12,7 @@ Polkadot
RPC
SHA
UI
URI
Wasm
Wasm32
WebAssembly
......
......@@ -267,6 +267,7 @@ examples-test-experimental-engine:
# We test only the examples for which the tests have already been migrated to
# use the experimental engine.
- cargo test --no-default-features --features std, ink-experimental-engine --verbose --manifest-path examples/erc20/Cargo.toml
- cargo test --no-default-features --features std, ink-experimental-engine --verbose --manifest-path examples/erc1155/Cargo.toml
- cargo test --no-default-features --features std, ink-experimental-engine --verbose --manifest-path examples/contract-terminate/Cargo.toml
- cargo test --no-default-features --features std, ink-experimental-engine --verbose --manifest-path examples/contract-transfer/Cargo.toml
......
# 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
\ No newline at end of file
[package]
name = "erc1155"
version = "3.0.0-rc3"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
[dependencies]
ink_primitives = { version = "3.0.0-rc3", path = "../../crates/primitives", default-features = false }
ink_metadata = { version = "3.0.0-rc3", path = "../../crates/metadata", default-features = false, features = ["derive"], optional = true }
ink_env = { version = "3.0.0-rc3", path = "../../crates/env", default-features = false, features = ["ink-debug"] }
ink_storage = { version = "3.0.0-rc3", path = "../../crates/storage", default-features = false }
ink_lang = { version = "3.0.0-rc3", path = "../../crates/lang", default-features = false }
ink_prelude = { version = "3.0.0-rc3", path = "../../crates/prelude", default-features = false }
scale = { package = "parity-scale-codec", version = "2.1", default-features = false, features = ["derive"] }
scale-info = { version = "0.6", default-features = false, features = ["derive"], optional = true }
[lib]
name = "erc1155"
path = "lib.rs"
crate-type = ["cdylib"]
[features]
default = ["std"]
std = [
"ink_primitives/std",
"ink_metadata",
"ink_metadata/std",
"ink_env/std",
"ink_storage/std",
"ink_lang/std",
"ink_prelude/std",
"scale/std",
"scale-info",
"scale-info/std",
]
ink-as-dependency = []
ink-experimental-engine = ["ink_env/ink-experimental-engine"]
// Copyright 2018-2021 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![cfg_attr(not(feature = "std"), no_std)]
use ink_env::AccountId;
use ink_lang as ink;
use ink_prelude::vec::Vec;
// This is the return value that we expect if a smart contract supports receiving ERC-1155
// tokens.
//
// It is calculated with
// `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`, and corresponds
// to 0xf23a6e61.
#[cfg_attr(test, allow(dead_code))]
const ON_ERC_1155_RECEIVED_SELECTOR: [u8; 4] = [0xF2, 0x3A, 0x6E, 0x61];
// This is the return value that we expect if a smart contract supports batch receiving ERC-1155
// tokens.
//
// It is calculated with
// `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`, and
// corresponds to 0xbc197c81.
const _ON_ERC_1155_BATCH_RECEIVED_SELECTOR: [u8; 4] = [0xBC, 0x19, 0x7C, 0x81];
/// A type representing the unique IDs of tokens managed by this contract.
pub type TokenId = u128;
type Balance = <ink_env::DefaultEnvironment as ink_env::Environment>::Balance;
// The ERC-1155 error types.
#[derive(Debug, PartialEq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
/// This token ID has not yet been created by the contract.
UnexistentToken,
/// The caller tried to sending tokens to the zero-address (`0x00`).
ZeroAddressTransfer,
/// The caller is not approved to transfer tokens on behalf of the account.
NotApproved,
/// The account does not have enough funds to complete the transfer.
InsufficientBalance,
/// An account does not need to approve themselves to transfer tokens.
SelfApproval,
/// The number of tokens being transferred does not match the specified number of transfers.
BatchTransferMismatch,
}
// The ERC-1155 result types.
pub type Result<T> = core::result::Result<T, Error>;
/// Evaluate `$x:expr` and if not true return `Err($y:expr)`.
///
/// Used as `ensure!(expression_to_ensure, expression_to_return_on_false)`.
macro_rules! ensure {
( $condition:expr, $error:expr $(,)? ) => {{
if !$condition {
return ::core::result::Result::Err(::core::convert::Into::into($error))
}
}};
}
/// The interface for an ERC-1155 compliant contract.
///
/// The interface is defined here: <https://eips.ethereum.org/EIPS/eip-1155>.
///
/// The goal of ERC-1155 is to allow a single deployed contract to manage a variety of assets.
/// These assets can be fungible, non-fungible, or a combination.
///
/// By tracking multiple assets the ERC-1155 standard is able to support batch transfers, which
/// make it easy to transfer a mix of multiple tokens at once.
#[ink::trait_definition]
pub trait Erc1155 {
/// Transfer a `value` amount of `token_id` tokens to the `to` account from the `from`
/// account.
///
/// Note that the call does not have to originate from the `from` account, and may originate
/// from any account which is approved to transfer `from`'s tokens.
#[ink(message)]
fn safe_transfer_from(
&mut self,
from: AccountId,
to: AccountId,
token_id: TokenId,
value: Balance,
data: Vec<u8>,
) -> Result<()>;
/// Perform a batch transfer of `token_ids` to the `to` account from the `from` account.
///
/// The number of `values` specified to be transfer must match the number of `token_ids`,
/// otherwise this call will revert.
///
/// Note that the call does not have to originate from the `from` account, and may originate
/// from any account which is approved to transfer `from`'s tokens.
#[ink(message)]
fn safe_batch_transfer_from(
&mut self,
from: AccountId,
to: AccountId,
token_ids: Vec<TokenId>,
values: Vec<Balance>,
data: Vec<u8>,
) -> Result<()>;
/// Query the balance of a specific token for the provided account.
#[ink(message)]
fn balance_of(&self, owner: AccountId, token_id: TokenId) -> Balance;
/// Query the balances for a set of tokens for a set of accounts.
///
/// E.g use this call if you want to query what Alice and Bob's balances are for Tokens ID 1 and
/// ID 2.
///
/// This will return all the balances for a given owner before moving on to the next owner. In
/// the example above this means that the return value should look like:
///
/// [Alice Balance of Token ID 1, Alice Balance of Token ID 2, Bob Balance of Token ID 1, Bob Balance of Token ID 2]
#[ink(message)]
fn balance_of_batch(
&self,
owners: Vec<AccountId>,
token_ids: Vec<TokenId>,
) -> Vec<Balance>;
/// Enable or disable a third party, known as an `operator`, to control all tokens on behalf of
/// the caller.
#[ink(message)]
fn set_approval_for_all(&mut self, operator: AccountId, approved: bool)
-> Result<()>;
/// Query if the given `operator` is allowed to control all of `owner`'s tokens.
#[ink(message)]
fn is_approved_for_all(&self, owner: AccountId, operator: AccountId) -> bool;
}
/// The interface for an ERC-1155 Token Receiver contract.
///
/// The interface is defined here: <https://eips.ethereum.org/EIPS/eip-1155>.
///
/// Smart contracts which want to accept token transfers must implement this interface. By default
/// if a contract does not support this interface any transactions originating from an ERC-1155
/// compliant contract which attempt to transfer tokens directly to the contract's address must be
/// reverted.
#[ink::trait_definition]
pub trait Erc1155TokenReceiver {
/// Handle the receipt of a single ERC-1155 token.
///
/// This should be called by a compliant ERC-1155 contract if the intended recipient is a smart
/// contract.
///
/// If the smart contract implementing this interface accepts token transfers then it must
/// return `ON_ERC_1155_RECEIVED_SELECTOR` from this function. To reject a transfer it must revert.
///
/// Any callers must revert if they receive anything other than `ON_ERC_1155_RECEIVED_SELECTOR` as a return
/// value.
#[ink(message)]
fn on_received(
&mut self,
operator: AccountId,
from: AccountId,
token_id: TokenId,
value: Balance,
data: Vec<u8>,
) -> Vec<u8>;
/// Handle the receipt of multiple ERC-1155 tokens.
///
/// This should be called by a compliant ERC-1155 contract if the intended recipient is a smart
/// contract.
///
/// If the smart contract implementing this interface accepts token transfers then it must
/// return `BATCH_ON_ERC_1155_RECEIVED_SELECTOR` from this function. To reject a transfer it must revert.
///
/// Any callers must revert if they receive anything other than `BATCH_ON_ERC_1155_RECEIVED_SELECTOR` as a return
/// value.
#[ink(message)]
fn on_batch_received(
&mut self,
operator: AccountId,
from: AccountId,
token_ids: Vec<TokenId>,
values: Vec<Balance>,
data: Vec<u8>,
) -> Vec<u8>;
}
#[ink::contract]
mod erc1155 {
use super::*;
use ink_prelude::collections::BTreeMap;
use ink_storage::traits::{
PackedLayout,
SpreadLayout,
};
/// Indicate that a token transfer has occured.
///
/// This must be emitted even if a zero value transfer occurs.
#[ink(event)]
pub struct TransferSingle {
#[ink(topic)]
operator: Option<AccountId>,
#[ink(topic)]
from: Option<AccountId>,
#[ink(topic)]
to: Option<AccountId>,
token_id: TokenId,
value: Balance,
}
/// Indicate that an approval event has happened.
#[ink(event)]
pub struct ApprovalForAll {
#[ink(topic)]
owner: AccountId,
#[ink(topic)]
operator: AccountId,
approved: bool,
}
/// Indicate that a token's URI has been updated.
#[ink(event)]
pub struct Uri {
value: ink_prelude::string::String,
#[ink(topic)]
token_id: TokenId,
}
/// Represents an (Owner, Operator) pair, in which the operator is allowed to spend funds on
/// behalf of the operator.
#[derive(
Copy,
Clone,
Debug,
Ord,
PartialOrd,
Eq,
PartialEq,
PackedLayout,
SpreadLayout,
scale::Encode,
scale::Decode,
)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
struct Approval {
owner: AccountId,
operator: AccountId,
}
/// An ERC-1155 contract.
#[ink(storage)]
#[derive(Default)]
pub struct Contract {
/// Tracks the balances of accounts across the different tokens that they might be holding.
balances: BTreeMap<(AccountId, TokenId), Balance>,
/// Which accounts (called operators) have been approved to spend funds on behalf of an owner.
approvals: BTreeMap<Approval, ()>,
/// A unique identifier for the tokens which have been minted (and are therefore supported)
/// by this contract.
token_id_nonce: TokenId,
}
impl Contract {
/// Initialize a default instance of this ERC-1155 implementation.
#[ink(constructor)]
pub fn new() -> Self {
Default::default()
}
/// Create the initial supply for a token.
///
/// The initial supply will be provided to the caller (a.k.a the minter), and the
/// `token_id` will be assigned by the smart contract.
///
/// Note that as implemented anyone can create tokens. If you were to deploy this to a
/// production environment you'd probably want to lock down the addresses that are allowed
/// to create tokens.
#[ink(message)]
pub fn create(&mut self, value: Balance) -> TokenId {
let caller = self.env().caller();
// Given that TokenId is a `u128` the likelihood of this overflowing is pretty slim.
self.token_id_nonce += 1;
self.balances.insert((caller, self.token_id_nonce), value);
// Emit transfer event but with mint semantics
self.env().emit_event(TransferSingle {
operator: Some(caller),
from: None,
to: if value == 0 { None } else { Some(caller) },
token_id: self.token_id_nonce,
value,
});
self.token_id_nonce
}
/// Mint a `value` amount of `token_id` tokens.
///
/// It is assumed that the token has already been `create`-ed. The newly minted supply will
/// be assigned to the caller (a.k.a the minter).
///
/// Note that as implemented anyone can mint tokens. If you were to deploy this to a
/// production environment you'd probably want to lock down the addresses that are allowed
/// to mint tokens.
#[ink(message)]
pub fn mint(&mut self, token_id: TokenId, value: Balance) -> Result<()> {
ensure!(token_id <= self.token_id_nonce, Error::UnexistentToken);
let caller = self.env().caller();
self.balances.insert((caller, token_id), value);
// Emit transfer event but with mint semantics
self.env().emit_event(TransferSingle {
operator: Some(caller),
from: None,
to: Some(caller),
token_id,
value,
});
Ok(())
}
// Helper function for performing single token transfers.
//
// Should not be used directly since it's missing certain checks which are important to the
// ERC-1155 standard (it is expected that the caller has already performed these).
fn perform_transfer(
&mut self,
from: AccountId,
to: AccountId,
token_id: TokenId,
value: Balance,
) {
self.balances
.entry((from, token_id))
.and_modify(|b| *b -= value);
self.balances
.entry((to, token_id))
.and_modify(|b| *b += value)
.or_insert(value);
let caller = self.env().caller();
self.env().emit_event(TransferSingle {
operator: Some(caller),
from: Some(from),
to: Some(from),
token_id,
value,
});
}
// Check if the address at `to` is a smart contract which accepts ERC-1155 token transfers.
//
// If they're a smart contract which **doesn't** accept tokens transfers this call will
// revert. Otherwise we risk locking user funds at in that contract with no chance of
// recovery.
#[cfg_attr(test, allow(unused_variables))]
fn transfer_acceptance_check(
&mut self,
caller: AccountId,
from: AccountId,
to: AccountId,
token_id: TokenId,
value: Balance,
data: Vec<u8>,
) {
// This is disabled during tests due to the use of `eval_contract()` not being
// supported (tests end up panicking).
#[cfg(not(test))]
{
use ink_env::call::{
build_call,
utils::ReturnType,
ExecutionInput,
Selector,
};
// If our recipient is a smart contract we need to see if they accept or
// reject this transfer. If they reject it we need to revert the call.
let params = build_call::<Environment>()
.callee(to)
.gas_limit(5000)
.exec_input(
ExecutionInput::new(Selector::new(ON_ERC_1155_RECEIVED_SELECTOR))
.push_arg(caller)
.push_arg(from)
.push_arg(token_id)
.push_arg(value)
.push_arg(data),
)
.returns::<ReturnType<Vec<u8>>>()
.params();
match ink_env::eval_contract(&params) {
Ok(v) => {
ink_env::debug_println!(
"Received return value \"{:?}\" from contract {:?}",
v,
from
);
assert_eq!(
v,
&ON_ERC_1155_RECEIVED_SELECTOR[..],
"The recipient contract at {:?} does not accept token transfers.\n
Expected: {:?}, Got {:?}", to, ON_ERC_1155_RECEIVED_SELECTOR, v
)
}
Err(e) => {
match e {
ink_env::Error::CodeNotFound
| ink_env::Error::NotCallable => {
// Our recipient wasn't a smart contract, so there's nothing more for
// us to do
ink_env::debug_println!("Recipient at {:?} from is not a smart contract ({:?})", from, e);
}
_ => {
// We got some sort of error from the call to our recipient smart
// contract, and as such we must revert this call
let msg = ink_prelude::format!(
"Got error \"{:?}\" while trying to call {:?}",
e,
from
);
ink_env::debug_println!("{}", &msg);
panic!("{}", &msg)
}
}
}
}
}
}
}
impl super::Erc1155 for Contract {
#[ink(message)]
fn safe_transfer_from(
&mut self,
from: AccountId,
to: AccountId,
token_id: TokenId,
value: Balance,
data: Vec<u8>,
) -> Result<()> {
let caller = self.env().caller();
if caller != from {
ensure!(self.is_approved_for_all(from, caller), Error::NotApproved);
}
ensure!(to != AccountId::default(), Error::ZeroAddressTransfer);
let balance = self.balance_of(from, token_id);
ensure!(balance >= value, Error::InsufficientBalance);
self.perform_transfer(from, to, token_id, value);
self.transfer_acceptance_check(caller, from, to, token_id, value, data);
Ok(())
}
#[ink(message)]
fn safe_batch_transfer_from(
&mut self,
from: AccountId,
to: AccountId,
token_ids: Vec<TokenId>,
values: Vec<Balance>,
data: Vec<u8>,
) -> Result<()> {
let caller = self.env().caller();
if caller != from {
ensure!(self.is_approved_for_all(from, caller), Error::NotApproved);
}
ensure!(to != AccountId::default(), Error::ZeroAddressTransfer);
ensure!(!token_ids.is_empty(), Error::BatchTransferMismatch);
ensure!(
token_ids.len() == values.len(),
Error::BatchTransferMismatch,
);
let transfers = token_ids.iter().zip(values.iter());
for (&id, &v) in transfers.clone() {
let balance = self.balance_of(from, id);
ensure!(balance >= v, Error::InsufficientBalance);
}
for (&id, &v) in transfers {
self.perform_transfer(from, to, id, v);
}
// Can use the any token ID/value here, we really just care about knowing if `to` is a
// smart contract which accepts transfers
self.transfer_acceptance_check(
caller,
from,
to,
token_ids[0],
values[0],
data,
);
Ok(())
}
#[ink(message)]
fn balance_of(&self, owner: AccountId, token_id: TokenId) -> Balance {
*self.balances.get(&(owner, token_id)).unwrap_or(&0)
}
#[ink(message)]
fn balance_of_batch(
&self,
owners: Vec<AccountId>,
token_ids: Vec<TokenId>,
) -> Vec<Balance> {
let mut output = Vec::new();
for o in &owners {
for t in &token_ids {
let amount = self.balance_of(*o, *t);
output.push(amount);
}
}
output
}
#[ink(message)]
fn set_approval_for_all(
&mut self,
operator: AccountId,
approved: bool,
) -> Result<()> {
let caller = self.env().caller();
<