Unverified Commit 771f1d04 authored by Michael Müller's avatar Michael Müller Committed by GitHub
Browse files

[examples] Add ERC-20 Trait-ified example (#523)

* [examples] Rustify return types

* [examples] Add trait-erc20 Example

* [examples] Improve readability

* [examples] Remove unneeded export

* [examples] Move definitions into module

* [examples] Commentify

* [examples] Improve comment

* [examples] Improve comment

* [examples] Fix typo

* [examples] Add Error::InsufficientAllowance

* [examples] Add Errors section in doc comments

* [examples] Make allowance_of_or_zero and balance_of_or_zero inplace

* [examples] Ensure allowance is only updated if transfer worked

* [examples] Apply cargo fmt

* [examples] Add test which would have catched allowance changing on failed transfer

* [examples] Apply cargo fmt

* [examples] Remove unnecessary assert for ()
parent 4581ae5c
Pipeline #111247 failed with stages
in 4 minutes and 57 seconds
......@@ -24,13 +24,19 @@ mod erc20 {
lazy::Lazy,
};
/// A simple ERC-20 contract.
#[ink(storage)]
pub struct Erc20 {
/// Total token supply.
total_supply: Lazy<Balance>,
/// Mapping from owner to number of owned token.
balances: StorageHashMap<AccountId, Balance>,
/// Mapping of the token amount which an account is allowed to withdraw
/// from another account.
allowances: StorageHashMap<(AccountId, AccountId), Balance>,
}
/// Event emitted when a token transfer occurs.
#[ink(event)]
pub struct Transfer {
#[ink(topic)]
......@@ -41,6 +47,8 @@ mod erc20 {
value: Balance,
}
/// Event emitted when an approval occurs that `spender` is allowed to withdraw
/// up to the amount of `value` tokens from `owner`.
#[ink(event)]
pub struct Approval {
#[ink(topic)]
......@@ -51,7 +59,21 @@ mod erc20 {
value: Balance,
}
/// The ERC-20 error types.
#[derive(Debug, PartialEq, Eq, scale::Encode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
/// Returned if not enough balance to fulfill a request is available.
InsufficientBalance,
/// Returned if not enough allowance to fulfill a request is available.
InsufficientAllowance,
}
/// The ERC-20 result type.
pub type Result<T> = core::result::Result<T, Error>;
impl Erc20 {
/// Creates a new ERC-20 contract with the specified initial supply.
#[ink(constructor)]
pub fn new(initial_supply: Balance) -> Self {
let caller = Self::env().caller();
......@@ -70,29 +92,50 @@ mod erc20 {
instance
}
/// Returns the total token supply.
#[ink(message)]
pub fn total_supply(&self) -> Balance {
*self.total_supply
}
/// Returns the account balance for the specified `owner`.
///
/// Returns `0` if the account is non-existent.
#[ink(message)]
pub fn balance_of(&self, owner: AccountId) -> Balance {
self.balance_of_or_zero(&owner)
self.balances.get(&owner).copied().unwrap_or(0)
}
/// Returns the amount which `spender` is still allowed to withdraw from `owner`.
///
/// Returns `0` if no allowance has been set `0`.
#[ink(message)]
pub fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance {
self.allowance_of_or_zero(&owner, &spender)
self.allowances.get(&(owner, spender)).copied().unwrap_or(0)
}
/// Transfers `value` amount of tokens from the caller's account to account `to`.
///
/// On success a `Transfer` event is emitted.
///
/// # Errors
///
/// Returns `InsufficientBalance` error if there are not enough tokens on
/// the caller's account balance.
#[ink(message)]
pub fn transfer(&mut self, to: AccountId, value: Balance) -> bool {
pub fn transfer(&mut self, to: AccountId, value: Balance) -> Result<()> {
let from = self.env().caller();
self.transfer_from_to(from, to, value)
}
/// Allows `spender` to withdraw from the caller's account multiple times, up to
/// the `value` amount.
///
/// If this function is called again it overwrites the current allowance with `value`.
///
/// An `Approval` event is emitted.
#[ink(message)]
pub fn approve(&mut self, spender: AccountId, value: Balance) -> bool {
pub fn approve(&mut self, spender: AccountId, value: Balance) -> Result<()> {
let owner = self.env().caller();
self.allowances.insert((owner, spender), value);
self.env().emit_event(Approval {
......@@ -100,56 +143,67 @@ mod erc20 {
spender,
value,
});
true
Ok(())
}
/// Transfers `value` tokens on the behalf of `from` to the account `to`.
///
/// This can be used to allow a contract to transfer tokens on ones behalf and/or
/// to charge fees in sub-currencies, for example.
///
/// On success a `Transfer` event is emitted.
///
/// # Errors
///
/// Returns `InsufficientAllowance` error if there are not enough tokens allowed
/// for the caller to withdraw from `from`.
///
/// Returns `InsufficientBalance` error if there are not enough tokens on
/// the the account balance of `from`.
#[ink(message)]
pub fn transfer_from(
&mut self,
from: AccountId,
to: AccountId,
value: Balance,
) -> bool {
) -> Result<()> {
let caller = self.env().caller();
let allowance = self.allowance_of_or_zero(&from, &caller);
let allowance = self.allowance(from, caller);
if allowance < value {
return false
return Err(Error::InsufficientAllowance)
}
self.transfer_from_to(from, to, value)?;
self.allowances.insert((from, caller), allowance - value);
self.transfer_from_to(from, to, value)
Ok(())
}
/// Transfers `value` amount of tokens from the caller's account to account `to`.
///
/// On success a `Transfer` event is emitted.
///
/// # Errors
///
/// Returns `InsufficientBalance` error if there are not enough tokens on
/// the caller's account balance.
fn transfer_from_to(
&mut self,
from: AccountId,
to: AccountId,
value: Balance,
) -> bool {
let from_balance = self.balance_of_or_zero(&from);
) -> Result<()> {
let from_balance = self.balance_of(from);
if from_balance < value {
return false
return Err(Error::InsufficientBalance)
}
self.balances.insert(from, from_balance - value);
let to_balance = self.balance_of_or_zero(&to);
let to_balance = self.balance_of(to);
self.balances.insert(to, to_balance + value);
self.env().emit_event(Transfer {
from: Some(from),
to: Some(to),
value,
});
true
}
fn balance_of_or_zero(&self, owner: &AccountId) -> Balance {
*self.balances.get(owner).unwrap_or(&0)
}
fn allowance_of_or_zero(
&self,
owner: &AccountId,
spender: &AccountId,
) -> Balance {
*self.allowances.get(&(*owner, *spender)).unwrap_or(&0)
Ok(())
}
}
......@@ -289,7 +343,7 @@ mod erc20 {
assert_eq!(erc20.balance_of(accounts.bob), 0);
// Alice transfers 10 tokens to Bob.
assert_eq!(erc20.transfer(accounts.bob, 10), true);
assert_eq!(erc20.transfer(accounts.bob, 10), Ok(()));
// Bob owns 10 tokens.
assert_eq!(erc20.balance_of(accounts.bob), 10);
......@@ -328,19 +382,19 @@ mod erc20 {
ink_env::test::CallData::new(ink_env::call::Selector::new([0x00; 4])); // balance_of
data.push_arg(&accounts.bob);
// Push the new execution context to set Bob as caller
assert_eq!(
ink_env::test::push_execution_context::<ink_env::DefaultEnvironment>(
accounts.bob,
callee,
1000000,
1000000,
data
),
()
ink_env::test::push_execution_context::<ink_env::DefaultEnvironment>(
accounts.bob,
callee,
1000000,
1000000,
data,
);
// Bob fails to transfers 10 tokens to Eve.
assert_eq!(erc20.transfer(accounts.eve, 10), false);
assert_eq!(
erc20.transfer(accounts.eve, 10),
Err(Error::InsufficientBalance)
);
// Alice owns all the tokens.
assert_eq!(erc20.balance_of(accounts.alice), 100);
assert_eq!(erc20.balance_of(accounts.bob), 0);
......@@ -367,9 +421,12 @@ mod erc20 {
.expect("Cannot get accounts");
// Bob fails to transfer tokens owned by Alice.
assert_eq!(erc20.transfer_from(accounts.alice, accounts.eve, 10), false);
assert_eq!(
erc20.transfer_from(accounts.alice, accounts.eve, 10),
Err(Error::InsufficientAllowance)
);
// Alice approves Bob for token transfers on her behalf.
assert_eq!(erc20.approve(accounts.bob, 10), true);
assert_eq!(erc20.approve(accounts.bob, 10), Ok(()));
// The approve event takes place.
assert_eq!(ink_env::test::recorded_events().count(), 2);
......@@ -382,19 +439,19 @@ mod erc20 {
ink_env::test::CallData::new(ink_env::call::Selector::new([0x00; 4])); // balance_of
data.push_arg(&accounts.bob);
// Push the new execution context to set Bob as caller.
assert_eq!(
ink_env::test::push_execution_context::<ink_env::DefaultEnvironment>(
accounts.bob,
callee,
1000000,
1000000,
data
),
()
ink_env::test::push_execution_context::<ink_env::DefaultEnvironment>(
accounts.bob,
callee,
1000000,
1000000,
data,
);
// Bob transfers tokens from Alice to Eve.
assert_eq!(erc20.transfer_from(accounts.alice, accounts.eve, 10), true);
assert_eq!(
erc20.transfer_from(accounts.alice, accounts.eve, 10),
Ok(())
);
// Eve owns tokens.
assert_eq!(erc20.balance_of(accounts.eve), 10);
......@@ -415,5 +472,51 @@ mod erc20 {
10,
);
}
#[ink::test]
fn allowance_must_not_change_on_failed_transfer() {
let mut erc20 = Erc20::new(100);
let accounts =
ink_env::test::default_accounts::<ink_env::DefaultEnvironment>()
.expect("Cannot get accounts");
// Alice approves Bob for token transfers on her behalf.
let alice_balance = erc20.balance_of(accounts.alice);
let initial_allowance = alice_balance + 2;
assert_eq!(erc20.approve(accounts.bob, initial_allowance), Ok(()));
// Get contract address.
let callee = ink_env::account_id::<ink_env::DefaultEnvironment>()
.unwrap_or([0x0; 32].into());
// Create call.
let mut data =
ink_env::test::CallData::new(ink_env::call::Selector::new([0x00; 4])); // balance_of
data.push_arg(&accounts.bob);
// Push the new execution context to set Bob as caller.
ink_env::test::push_execution_context::<ink_env::DefaultEnvironment>(
accounts.bob,
callee,
1000000,
1000000,
data,
);
// Bob tries to transfer tokens from Alice to Eve.
let emitted_events_before =
ink_env::test::recorded_events().collect::<Vec<_>>();
assert_eq!(
erc20.transfer_from(accounts.alice, accounts.eve, alice_balance + 1),
Err(Error::InsufficientBalance)
);
// Allowance must have stayed the same
assert_eq!(
erc20.allowance(accounts.alice, accounts.bob),
initial_allowance
);
// No more events must have been emitted
let emitted_events_after =
ink_env::test::recorded_events().collect::<Vec<_>>();
assert_eq!(emitted_events_before.len(), emitted_events_after.len());
}
}
}
......@@ -118,7 +118,7 @@ mod erc721 {
id: TokenId,
}
/// Event emited when a token approve occurs.
/// Event emitted when a token approve occurs.
#[ink(event)]
pub struct Approval {
#[ink(topic)]
......@@ -532,15 +532,12 @@ mod erc721 {
ink_env::test::CallData::new(ink_env::call::Selector::new([0x00; 4])); // balance_of
data.push_arg(&accounts.bob);
// Push the new execution context to set Bob as caller
assert_eq!(
ink_env::test::push_execution_context::<ink_env::DefaultEnvironment>(
accounts.bob,
callee,
1000000,
1000000,
data
),
()
ink_env::test::push_execution_context::<ink_env::DefaultEnvironment>(
accounts.bob,
callee,
1000000,
1000000,
data,
);
// Bob cannot transfer not owned tokens.
assert_eq!(erc721.transfer(accounts.eve, 2), Err(Error::NotApproved));
......@@ -567,15 +564,12 @@ mod erc721 {
ink_env::test::CallData::new(ink_env::call::Selector::new([0x00; 4])); // balance_of
data.push_arg(&accounts.bob);
// Push the new execution context to set Bob as caller
assert_eq!(
ink_env::test::push_execution_context::<ink_env::DefaultEnvironment>(
accounts.bob,
callee,
1000000,
1000000,
data
),
()
ink_env::test::push_execution_context::<ink_env::DefaultEnvironment>(
accounts.bob,
callee,
1000000,
1000000,
data,
);
// Bob transfers token Id 1 from Alice to Eve.
assert_eq!(
......@@ -620,15 +614,12 @@ mod erc721 {
ink_env::test::CallData::new(ink_env::call::Selector::new([0x00; 4])); // balance_of
data.push_arg(&accounts.bob);
// Push the new execution context to set Bob as caller
assert_eq!(
ink_env::test::push_execution_context::<ink_env::DefaultEnvironment>(
accounts.bob,
callee,
1000000,
1000000,
data
),
()
ink_env::test::push_execution_context::<ink_env::DefaultEnvironment>(
accounts.bob,
callee,
1000000,
1000000,
data,
);
// Bob transfers token Id 1 from Alice to Eve.
assert_eq!(
......@@ -682,15 +673,12 @@ mod erc721 {
ink_env::test::CallData::new(ink_env::call::Selector::new([0x00; 4])); // balance_of
data.push_arg(&accounts.bob);
// Push the new execution context to set Eve as caller
assert_eq!(
ink_env::test::push_execution_context::<ink_env::DefaultEnvironment>(
accounts.eve,
callee,
1000000,
1000000,
data
),
()
ink_env::test::push_execution_context::<ink_env::DefaultEnvironment>(
accounts.eve,
callee,
1000000,
1000000,
data,
);
// Eve is not an approved operator by Alice.
assert_eq!(
......
# 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 = "erc20"
version = "3.0.0-rc1"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
[dependencies]
ink_primitives = { version = "3.0.0-rc1", path = "../../crates/primitives", default-features = false }
ink_metadata = { version = "3.0.0-rc1", path = "../../crates/metadata", default-features = false, features = ["derive"], optional = true }
ink_env = { version = "3.0.0-rc1", path = "../../crates/env", default-features = false }
ink_storage = { version = "3.0.0-rc1", path = "../../crates/storage", default-features = false }
ink_lang = { version = "3.0.0-rc1", path = "../../crates/lang", default-features = false }
scale = { package = "parity-scale-codec", version = "1.3", default-features = false, features = ["derive"] }
scale-info = { version = "0.4", default-features = false, features = ["derive"], optional = true }
[lib]
name = "erc20"
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",
"scale/std",
"scale-info",
"scale-info/std",
]
ink-as-dependency = []
This diff is collapsed.
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment