Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation #1

Merged
merged 8 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "psp22"
version = "0.1.0"
authors = ["Hans <michal.handzlik@cardinals.cc>"]
edition = "2021"

[dependencies]
ink = { version = "4.3", default-features = false }

scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2.9", default-features = false, features = ["derive"], optional = true }

[lib]
path = "lib.rs"

[features]
default = ["std"]
std = [
"ink/std",
"scale/std",
"scale-info/std",
]
contract = []
ink-as-dependency = []
204 changes: 204 additions & 0 deletions lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
#![cfg_attr(not(feature = "std"), no_std, no_main)]

mod traits;
mod types;

pub use traits::PSP22;
pub use types::{PSP22Data, PSP22Error};

#[cfg(feature = "contract")]
#[ink::contract]
mod token {
use crate::traits::PSP22;
use crate::types::{PSP22Data, PSP22Error};

#[ink(storage)]
pub struct Token {
data: PSP22Data,
}

impl Token {
#[ink(constructor)]
pub fn new(supply: u128) -> Self {
Self {
data: PSP22Data::new(supply, Self::env().caller()),
}
}
}

#[ink(event)]
pub struct Approval {
#[ink(topic)]
owner: AccountId,
#[ink(topic)]
spender: AccountId,
amount: u128,
}

#[ink(event)]
pub struct Transfer {
#[ink(topic)]
from: AccountId,
#[ink(topic)]
to: AccountId,
value: u128,
}

impl PSP22 for Token {
#[ink(message)]
fn total_supply(&self) -> u128 {
self.data.total_supply
}

#[ink(message)]
fn balance_of(&self, owner: AccountId) -> u128 {
self.data.balances.get(owner).unwrap_or_default()
}

#[ink(message)]
fn allowance(&self, owner: AccountId, spender: AccountId) -> u128 {
self.data
.allowances
.get((owner, spender))
.unwrap_or_default()
}

#[ink(message)]
fn transfer(
&mut self,
to: AccountId,
value: u128,
_data: ink::prelude::vec::Vec<u8>,
) -> Result<(), PSP22Error> {
let from = self.env().caller();
if from == to {
return Ok(());
}
let from_balance = self.balance_of(from);
if from_balance < value {
return Err(PSP22Error::InsufficientBalance);
}

if from_balance == value {
self.data.balances.remove(from);
} else {
self.data.balances.insert(from, &(from_balance - value));
}
let to_balance = self.balance_of(to);
// Total supply is limited by u128.MAX so no overflow is possible
self.data.balances.insert(to, &(to_balance + value));
self.env().emit_event(Transfer { from, to, value });
Ok(())
}

#[ink(message)]
fn transfer_from(
&mut self,
from: AccountId,
to: AccountId,
value: u128,
data: ink::prelude::vec::Vec<u8>,
) -> Result<(), PSP22Error> {
if from == to {
return Ok(());
}
let caller = self.env().caller();
if caller == from {
return self.transfer(to, value, data);
}

let allowance = self.allowance(from, caller);
if allowance < value {
return Err(PSP22Error::InsufficientAllowance);
}
let from_balance = self.balance_of(from);
if from_balance < value {
return Err(PSP22Error::InsufficientBalance);
}

if allowance == value {
self.data.allowances.remove((from, caller));
} else {
self.data
.allowances
.insert((from, caller), &(allowance - value));
}
self.env().emit_event(Approval {
owner: from,
spender: caller,
amount: allowance - value,
});
deuszx marked this conversation as resolved.
Show resolved Hide resolved

if from_balance == value {
self.data.balances.remove(from);
} else {
self.data.balances.insert(from, &(from_balance - value));
}
let to_balance = self.balance_of(to);
// Total supply is limited by u128.MAX so no overflow is possible
self.data.balances.insert(to, &(to_balance + value));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if relevant, but take a look at this use-ink/ink#1831 regarding overflow checks, because I think there are some subtleties

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed it looks like soon all unchecked arithmetic is going to be banned from ink! contracts. I replaced all occurrences of + and - (which were impossible to overflow "by design") with saturating_add and saturating_sub.

self.env().emit_event(Transfer { from, to, value });
Ok(())
}

#[ink(message)]
fn approve(&mut self, spender: AccountId, amount: u128) -> Result<(), PSP22Error> {
let owner = self.env().caller();
if owner == spender {
return Ok(());
}
if amount == 0 {
self.data.allowances.remove((owner, spender));
} else {
self.data.allowances.insert((owner, spender), &amount);
}
self.env().emit_event(Approval {
owner,
spender,
amount,
});
Ok(())
}

#[ink(message)]
fn increase_allowance(&mut self, spender: AccountId, by: u128) -> Result<(), PSP22Error> {
let owner = self.env().caller();
if owner == spender {
return Ok(());
}
let allowance = self.allowance(owner, spender);
let amount = allowance.saturating_add(by);
self.data.allowances.insert((owner, spender), &amount);
self.env().emit_event(Approval {
owner,
spender,
amount,
});
Ok(())
}

#[ink(message)]
fn decrease_allowance(&mut self, spender: AccountId, by: u128) -> Result<(), PSP22Error> {
let owner = self.env().caller();
if owner == spender {
return Ok(());
}
let allowance = self.allowance(owner, spender);
if allowance < by {
return Err(PSP22Error::InsufficientAllowance);
h4nsu marked this conversation as resolved.
Show resolved Hide resolved
}
let amount = allowance - by;
if amount == 0 {
self.data.allowances.remove((owner, spender));
} else {
self.data.allowances.insert((owner, spender), &amount);
}
Comment on lines +207 to +211
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether it wouldn't be slightly better to do it like:

  1. self.data.allowances.remove((owner, spender)) in line L186 - remove returns "previous" value.
  2. Check if new_allowance (amount) equals 0:
  • if "yes", do nothing
  • if "no", call insert.

Seems like that would be 1 access to the Mapping less.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, it could be a little bit more efficient.

Currently there are 3 branches this code can end up with and the resulting Mapping operations are:

  1. allowance < by : 1 get
  2. allowance = by : 1 get + 1 remove
  3. allowance > by : 1 get + 1 insert

In the version you propose that would be:

  1. allowance < by : 1 remove + 1 insert
  2. allowance = by : 1 remove
  3. allowance > by : 1 remove + 1 insert

Not sure what is the relative cost of remove vs get, but the total number of operations globally is the same. Obviously, we expect variant 2 to "fire" much more often than variant 1, so expected number of operations is lower in your version. But I feel like this is a miniscule gain that sacrifices quite a lot of readability in a code that is supposed to be shown to community, so I'd lean towards simplicity > efficiency in this case

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the version you propose that would be:

  1. allowance < by : 1 remove + 1 insert
  2. allowance = by : 1 remove
  3. allowance > by : 1 remove + 1 insert
  1. current_allowance < by : 1 remove (no insert since we would fail early with InsufficientAllowance
  2. current_allowance = by : 1 remove
  3. current_allowance > by: 1 remove + 1 insert

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're not 100% sure Mapping.take() is stable, lets stay with the simpler 100% safe implementation for now

self.env().emit_event(Approval {
owner,
spender,
amount,
});
Ok(())
}
}
}
117 changes: 117 additions & 0 deletions traits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use ink::{prelude::vec::Vec, primitives::AccountId};

use crate::types::PSP22Error;

#[ink::trait_definition]
pub trait PSP22 {
/// Returns the total token supply.
#[ink(message)]
fn total_supply(&self) -> u128;

/// Returns the account balance for the specified `owner`.
///
/// Returns `0` if the account is non-existent.
#[ink(message)]
fn balance_of(&self, owner: AccountId) -> u128;

/// Returns the amount which `spender` is still allowed to withdraw from `owner`.
///
/// Returns `0` if no allowance has been set.
#[ink(message)]
fn allowance(&self, owner: AccountId, spender: AccountId) -> u128;

/// Transfers `value` amount of tokens from the caller's account to account `to`
/// with additional `data` in unspecified format.
///
/// # Events
///
/// On success a `Transfer` event is emitted.
///
/// No-op if the caller and `to` is the same address, returns success and no events are emitted.
///
/// # Errors
///
/// Reverts with `InsufficientBalance` if the `value` exceeds the caller's balance.
#[ink(message)]
fn transfer(&mut self, to: AccountId, value: u128, data: Vec<u8>) -> Result<(), PSP22Error>;

/// Transfers `value` tokens on the behalf of `from` to the account `to`
/// with additional `data` in unspecified format.
///
/// If `from` and the caller are different addresses, the caller must be allowed
/// by `from` to spend at least `value` tokens.
///
/// # Events
///
/// On success a `Transfer` event is emitted.
///
/// No-op if `from` and `to` is the same address, returns success and no events are emitted.
///
/// If `from` and the caller are different addresses, a successful transfer results
/// in decreased allowance by `from` to the caller and an `Approval` event with
/// the new allowance amount is emitted.
///
/// # Errors
///
/// Reverts with `InsufficientBalance` if the `value` exceeds the balance of the account `from`.
///
/// Reverts with `InsufficientAllowance` if `from` and the caller are different addresses and
/// the `value` exceeds the allowance granted by `from` to the caller.
///
/// If conditions for both `InsufficientBalance` and `InsufficientAllowance` errors are met,
/// reverts with `InsufficientAllowance`.
#[ink(message)]
fn transfer_from(
&mut self,
from: AccountId,
to: AccountId,
value: u128,
data: Vec<u8>,
) -> Result<(), PSP22Error>;

/// Allows `spender` to withdraw from the caller's account multiple times, up to
/// the total amount of `value`.
///
/// Successive calls of this method overwrite previous values.
///
/// # Events
///
/// An `Approval` event is emitted.
///
/// No-op if the caller and `spender` is the same address, returns success and no events are emitted.
#[ink(message)]
fn approve(&mut self, spender: AccountId, value: u128) -> Result<(), PSP22Error>;

/// Increases by `delta-value` the allowance granted to `spender` by the caller.
///
/// # Events
///
/// An `Approval` event with the new allowance amount is emitted.
///
/// No-op if the caller and `spender` is the same address, returns success and no events are emitted.
#[ink(message)]
fn increase_allowance(
&mut self,
spender: AccountId,
delta_value: u128,
) -> Result<(), PSP22Error>;

/// Decreases by `delta-value` the allowance granted to `spender` by the caller.
///
/// # Events
///
/// An `Approval` event with the new allowance amount is emitted.
///
/// No-op if the caller and `spender` is the same address, returns success and no events are emitted.
///
/// # Errors
///
/// Reverts with `InsufficientAllowance` if `spender` and the caller are different addresses and
/// the `delta-value` exceeds the allowance granted by the caller to `spender`.
#[ink(message)]
fn decrease_allowance(
&mut self,
spender: AccountId,
delta_value: u128,
) -> Result<(), PSP22Error>;
}
38 changes: 38 additions & 0 deletions types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use ink::{prelude::string::String, primitives::AccountId, storage::Mapping};

#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum PSP22Error {
/// Custom error type for cases in which an implementation adds its own restrictions.
Custom(String),
/// Returned if not enough Balance to fulfill a request is available.
InsufficientBalance,
/// Returned if not enough allowance to fulfill a request is available.
InsufficientAllowance,
/// Returned if recipient's address is zero.
ZeroRecipientAddress,
/// Returned if sender's address is zero.
ZeroSenderAddress,
deuszx marked this conversation as resolved.
Show resolved Hide resolved
/// Returned if a safe transfer check fails (e.g. if the receiving contract does not accept tokens).
SafeTransferCheckFailed(String),
}

#[ink::storage_item]
#[derive(Debug, Default)]
pub struct PSP22Data {
pub total_supply: u128,
pub balances: Mapping<AccountId, u128>,
pub allowances: Mapping<(AccountId, AccountId), u128>,
}

impl PSP22Data {
pub fn new(supply: u128, creator: AccountId) -> PSP22Data {
let mut data = PSP22Data {
total_supply: supply,
balances: Default::default(),
allowances: Default::default(),
};
data.balances.insert(creator, &supply);
data
}
}