6 releases (2 stable)

2.0.0 May 7, 2024
1.0.0 May 6, 2024
0.3.0 Mar 26, 2024
0.2.2 Feb 29, 2024
0.2.1 Nov 10, 2023

#8 in #ink

Download history 1132/week @ 2024-06-01 125/week @ 2024-06-08 123/week @ 2024-06-15 118/week @ 2024-06-22 46/week @ 2024-06-29 47/week @ 2024-07-06 180/week @ 2024-07-13 109/week @ 2024-07-20 227/week @ 2024-07-27 481/week @ 2024-08-03 254/week @ 2024-08-10 48/week @ 2024-08-17 58/week @ 2024-08-24 93/week @ 2024-08-31 34/week @ 2024-09-07 70/week @ 2024-09-14

257 downloads per month
Used in wrapped-azero

Apache-2.0

57KB
957 lines

PSP22 Fungible Token

PSP22 is a fungible token standard for WebAssembly smart contracts running on blockchains based on the Substrate framework. It is an equivalent of Ethereum's ERC-20. The definition of the PSP22 standard can be found here.

This repository contains a simple, minimal implementation of the PSP22 token in ink! smart contract programming language (EDSL based on Rust).

How to use this repository

[!IMPORTANT] This version of the psp22 crate is compatible with ink! 5. For ink! 4 compatibility, please use 1.x.x version.

To use this crate as a dependency please add the following lines in your project's Cargo.toml:

psp22 = { version = "2.0", default-features = false, features = ["ink-as-dependency"] }
# ...
[features]
# ...
std = [
    # ...
    "psp22/std",
]

The contents of this repository can be used in following ways:

1. Ready to use contract

The file lib.rs contains a ready to use implementation of basic PSP22 token contract (extended with PSP22Metadata). To use it, please check out this repository and compile its contents using cargo-contract:

cargo contract build --release

2. Cross contract calling with traits

The PSP22 trait contains all the methods defined in the PSP22 standard. The trait can be used together with ink!'s contract_ref macro to allow for convenient cross-contract calling.

In your contract, if you would like to make a call to some other contract implementing the PSP22 standard, all you need to do is:

use ink::contract_ref;
use ink::prelude::vec;
use psp22::PSP22;

let mut token: contract_ref!(PSP22) = psp22_contract_address.into();

// Now `token` has all PSP22 methods
let balance = token.balance_of(some_account);
token.transfer(recipient, balance, vec![]); // returns Result<(), PSP22Error>

The same method can be used with other traits (PSP22Metadata, PSP22Burnable, PSP22Mintable) defined in this crate. See the contents of traits.rs for details.

3. Custom implementation of PSP22 logic with PSP22Data

The PSP22Data class can be used to extend your contract with PSP22 token logic. In other words, you can easily build contracts that implement PSP22 interface alongside some other functionalities defined by the business logic of your project.

The methods of the PSP22Data class correspond directly to queries and operations defined by the PSP22 token standard. To make your contract become a PSP22 token, you need to:

  • Put a single PSP22Data instance in your contract's storage and initialize it with some starting supply of tokens.
  • Add the impl PSP22 for [struct_name] block with implementation of PSP22 trait messages using PSP22Data methods. Each method which mutates the state of the token database returns a Result<Vec<PSP22Event>, PSP22Error> with all events generated by that operation. Please make sure to handle errors correctly and emit the resulting events (see the emit_events function).
  • Optionally implement also the PSP22Metadata trait to make your token play nice with ecosystem tools.
  • Optionally add unit tests with tests! macro (see below)

The contract in lib.rs contains an example implementation following all the above steps. Feel free to copy-paste parts of it.

4. Unit testing

This crate comes with a suite of unit tests for PSP22 tokens. It can be easily added to your contract's unit tests with a helper macro tests!. The macro should be invoked inside the main contract's module (the one annotated with #[ink::contract]):

#[ink::contract]
mod mycontract {
    ...
    #[ink(storage)]
    pub struct MyContract { ... }
    ...

    #[cfg(test)]
    mod tests {
        use super::MyContract;
        psp22::tests!(MyContract, (|total_supply| MyContract::new(..., total_supply, ...)));
    }

}

As you can see in the code snippet above, the psp22::tests! macro takes two arguments. The first one should be a name of a struct which implements PSP22 trait (usually your contract storage struct). The second argument should be a token constructor for a given total supply. In other words, the second argument should be an expression that takes a single u128 argument and returns the PSP22 struct initialized to have that amount as total supply (with all tokens initially assigned to the caller's account).

5. Burnable and Mintable extensions

The PSP22Data class contains also burn and mint methods, which can be used to implement PSP22Burnable and PSP22Mintable extensions and make your token burnable and/or mintable. An example implementation follows the same pattern as for the base trait:

impl PSP22Burnable for Token {
    #[ink(message)]
    fn burn(&mut self, value: u128) -> Result<(), PSP22Error> {
        let events = self.data.burn(self.env().caller(), value)?;
        self.emit_events(events);
        Ok(())
    }
}

Please note that PSP22Data burn and mint methods do not enforce any form of access control. It's probably not a good idea to have a token which can be minted and burned by anyone anytime. When implementing Burnable and Mintable extensions, please make sure that their usage is restricted according to your project's business logic. For example:

#[ink(storage)]
pub struct Token {
    data: PSP22Data,
    name: Option<String>,
    symbol: Option<String>,
    decimals: u8,
    owner: AccountId, // creator of the token
}

impl Token {
    #[ink(constructor)]
    pub fn new(
        supply: u128,
        name: Option<String>,
        symbol: Option<String>,
        decimals: u8,
    ) -> Self {
        Self {
            data: PSP22Data::new(supply, Self::env().caller()),
            name,
            symbol,
            decimals,
            owner: Self::env().caller(),
        }
    }
// ...
}

impl PSP22Burnable for Token {
    #[ink(message)]
    fn burn(&mut self, value: u128) -> Result<(), PSP22Error> {
        if self.env().caller() != self.owner {
            return PSP22Error::Custom(String::from("Only owner can burn"));
        }
        let events = self.data.burn(self.env().caller(), value)?;
        self.emit_events(events);
        Ok(())
    }
}

Dependencies

~5.5–9MB
~168K SLoC