Coins pallet (#399)

* initial implementation

* add function to get a balance of an account

* add support for multiple coins

* rename pallet to "coins-pallet"

* replace balances, assets and tokens pallet with coins pallet in runtime

* add total supply info

* update client side for new Coins pallet

* handle fees

* bug fixes

* Update FeeAccount test

* Fmt

* fix pr comments

* remove extraneous Imbalance type

* Minor tweaks

---------

Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
This commit is contained in:
akildemir
2023-10-19 13:22:21 +03:00
committed by GitHub
parent 3255c0ace5
commit fdfce9e207
32 changed files with 535 additions and 445 deletions

View File

@@ -0,0 +1,49 @@
[package]
name = "serai-coins-pallet"
version = "0.1.0"
description = "Coins pallet for Serai"
license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/coins/pallet"
authors = ["Akil Demir <aeg_asd@hotmail.com>"]
edition = "2021"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
parity-scale-codec = { version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"] }
frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
pallet-transaction-payment = { git = "https://github.com/serai-dex/substrate", default-features = false }
serai-primitives = { path = "../../primitives", default-features = false }
coins-primitives = { package = "serai-coins-primitives", path = "../primitives", default-features = false }
[features]
std = [
"frame-system/std",
"frame-support/std",
"sp-std/std",
"sp-runtime/std",
"pallet-transaction-payment/std",
"serai-primitives/std",
"coins-primitives/std",
]
runtime-benchmarks = [
"frame-system/runtime-benchmarks",
"frame-support/runtime-benchmarks",
]
default = ["std"]

View File

@@ -0,0 +1,15 @@
AGPL-3.0-only license
Copyright (c) 2023 Luke Parker
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License Version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@@ -0,0 +1,278 @@
#![cfg_attr(not(feature = "std"), no_std)]
#[frame_support::pallet]
pub mod pallet {
use sp_std::vec::Vec;
use sp_core::sr25519::Public;
use sp_runtime::{
traits::{DispatchInfoOf, PostDispatchInfoOf},
transaction_validity::{TransactionValidityError, InvalidTransaction},
};
use frame_system::pallet_prelude::*;
use frame_support::pallet_prelude::*;
use pallet_transaction_payment::{Config as TpConfig, OnChargeTransaction};
use serai_primitives::*;
pub use coins_primitives as primitives;
use primitives::*;
#[pallet::config]
pub trait Config: frame_system::Config<AccountId = Public> + TpConfig {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
#[pallet::genesis_config]
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
pub struct GenesisConfig<T: Config> {
_config: PhantomData<T>,
pub accounts: Vec<(Public, Balance)>,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
GenesisConfig { _config: PhantomData, accounts: Default::default() }
}
}
#[pallet::error]
pub enum Error<T> {
AmountOverflowed,
NotEnoughCoins,
SriBurnNotAllowed,
}
#[pallet::event]
#[pallet::generate_deposit(fn deposit_event)]
pub enum Event<T: Config> {
Mint { to: Public, balance: Balance },
Burn { from: Public, instruction: OutInstructionWithBalance },
SriBurn { from: Public, amount: Amount },
Transfer { from: Public, to: Public, balance: Balance },
}
#[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>);
/// The amount of coins each account has.
// Identity is used as the second key's hasher due to it being a non-manipulatable fixed-space
// ID.
#[pallet::storage]
#[pallet::getter(fn balances)]
pub type Balances<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
Public,
Identity,
Coin,
SubstrateAmount,
ValueQuery,
>;
/// The total supply of each coin.
// We use Identity type here again due to reasons stated in the Balances Storage.
#[pallet::storage]
#[pallet::getter(fn supply)]
pub type Supply<T: Config> = StorageMap<_, Identity, Coin, SubstrateAmount, ValueQuery>;
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
// initialize the supply of the coins
for c in COINS.iter() {
Supply::<T>::set(c, 0);
}
// initialize the genesis accounts
for (account, balance) in self.accounts.iter() {
Pallet::<T>::mint(*account, *balance).unwrap();
}
}
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_: BlockNumberFor<T>) -> Weight {
// burn the fees collected previous block
let coin = Coin::Serai;
let amount = Self::balance(FEE_ACCOUNT.into(), coin);
// we can unwrap, we are not burning more then what we have
// If this errors, it'll halt the runtime however (due to being called at the start of every
// block), requiring extra care when reviewing
Self::burn_sri(FEE_ACCOUNT.into(), amount).unwrap();
Weight::zero() // TODO
}
}
impl<T: Config> Pallet<T> {
/// Returns the balance of a given account for `coin`.
pub fn balance(of: Public, coin: Coin) -> Amount {
Amount(Self::balances(of, coin))
}
fn decrease_balance_internal(from: Public, balance: Balance) -> Result<(), Error<T>> {
let coin = &balance.coin;
// sub amount from account
let new_amount = Self::balances(from, coin)
.checked_sub(balance.amount.0)
.ok_or(Error::<T>::NotEnoughCoins)?;
// save
if new_amount == 0 {
Balances::<T>::remove(from, coin);
} else {
Balances::<T>::set(from, coin, new_amount);
}
Ok(())
}
fn increase_balance_internal(to: Public, balance: Balance) -> Result<(), Error<T>> {
let coin = &balance.coin;
// sub amount from account
let new_amount = Self::balances(to, coin)
.checked_add(balance.amount.0)
.ok_or(Error::<T>::AmountOverflowed)?;
// save
Balances::<T>::set(to, coin, new_amount);
Ok(())
}
/// Mint `balance` to the given account.
///
/// Errors if any amount overflows.
pub fn mint(to: Public, balance: Balance) -> Result<(), Error<T>> {
// update the balance
Self::increase_balance_internal(to, balance)?;
// update the supply
let new_supply = Self::supply(balance.coin)
.checked_add(balance.amount.0)
.ok_or(Error::<T>::AmountOverflowed)?;
Supply::<T>::set(balance.coin, new_supply);
Self::deposit_event(Event::Mint { to, balance });
Ok(())
}
// Burn `balance` from the specified account.
fn burn_internal(
from: Public,
balance: Balance,
) -> Result<(), Error<T>> {
// don't waste time if amount == 0
if balance.amount.0 == 0 {
return Ok(());
}
// update the balance
Self::decrease_balance_internal(from, balance)?;
// update the supply
let new_supply = Self::supply(balance.coin)
.checked_sub(balance.amount.0)
.unwrap();
Supply::<T>::set(balance.coin, new_supply);
Ok(())
}
pub fn burn_sri(
from: Public,
amount: Amount,
) -> Result<(), Error<T>> {
Self::burn_internal(from, Balance { coin: Coin::Serai, amount })?;
Self::deposit_event(Event::SriBurn { from, amount });
Ok(())
}
pub fn burn_non_sri(
from: Public,
instruction: OutInstructionWithBalance,
) -> Result<(), Error<T>> {
if instruction.balance.coin == Coin::Serai {
Err(Error::<T>::SriBurnNotAllowed)?;
}
Self::burn_internal(from, instruction.balance)?;
Self::deposit_event(Event::Burn { from, instruction });
Ok(())
}
/// Transfer `balance` from `from` to `to`.
pub fn transfer_internal(
from: Public,
to: Public,
balance: Balance,
) -> Result<(), Error<T>> {
// update balances of accounts
Self::decrease_balance_internal(from, balance)?;
Self::increase_balance_internal(to, balance)?;
Self::deposit_event(Event::Transfer { from, to, balance });
Ok(())
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight((0, DispatchClass::Normal))] // TODO
pub fn transfer(origin: OriginFor<T>, to: Public, balance: Balance) -> DispatchResult {
let from = ensure_signed(origin)?;
Self::transfer_internal(from, to, balance)?;
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight((0, DispatchClass::Normal))] // TODO
pub fn burn(origin: OriginFor<T>, instruction: OutInstructionWithBalance) -> DispatchResult {
let from = ensure_signed(origin)?;
Self::burn_non_sri(from, instruction)?;
Ok(())
}
}
impl<T: Config> OnChargeTransaction<T> for Pallet<T> {
type Balance = SubstrateAmount;
type LiquidityInfo = Option<SubstrateAmount>;
fn withdraw_fee(
who: &Public,
_call: &T::RuntimeCall,
_dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
fee: Self::Balance,
_tip: Self::Balance,
) -> Result<Self::LiquidityInfo, TransactionValidityError> {
if fee == 0 {
return Ok(None);
}
let balance = Balance { coin: Coin::Serai, amount: Amount(fee) };
match Self::transfer_internal(*who, FEE_ACCOUNT.into(), balance) {
Err(_) => Err(InvalidTransaction::Payment)?,
Ok(()) => Ok(Some(fee)),
}
}
fn correct_and_deposit_fee(
who: &Public,
_dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
_post_info: &PostDispatchInfoOf<T::RuntimeCall>,
corrected_fee: Self::Balance,
_tip: Self::Balance,
already_withdrawn: Self::LiquidityInfo,
) -> Result<(), TransactionValidityError> {
if let Some(paid) = already_withdrawn {
let refund_amount = paid.saturating_sub(corrected_fee);
let balance = Balance { coin: Coin::Serai, amount: Amount(refund_amount) };
Self::transfer_internal(FEE_ACCOUNT.into(), *who, balance)
.map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Payment))?;
}
Ok(())
}
}
}
pub use pallet::*;

View File

@@ -0,0 +1,28 @@
[package]
name = "serai-coins-primitives"
version = "0.1.0"
description = "Serai coins primitives"
license = "MIT"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
zeroize = { version = "^1.5", features = ["derive"], optional = true }
serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"] }
serai-primitives = { path = "../../primitives", default-features = false }
[dev-dependencies]
sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
[features]
std = ["zeroize", "serde/std", "scale/std", "scale-info/std", "sp-runtime/std", "serai-primitives/std"]
default = ["std"]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Luke Parker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,51 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "std")]
use zeroize::Zeroize;
use serde::{Serialize, Deserialize};
use scale::{Encode, Decode, MaxEncodedLen};
use scale_info::TypeInfo;
use serai_primitives::{Balance, SeraiAddress, ExternalAddress, Data, system_address};
pub const FEE_ACCOUNT: SeraiAddress = system_address(b"FeeAccount");
#[derive(
Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Encode, Decode, MaxEncodedLen, TypeInfo,
)]
#[cfg_attr(feature = "std", derive(Zeroize))]
pub struct OutInstruction {
pub address: ExternalAddress,
pub data: Option<Data>,
}
#[derive(
Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Encode, Decode, MaxEncodedLen, TypeInfo,
)]
#[cfg_attr(feature = "std", derive(Zeroize))]
pub struct OutInstructionWithBalance {
pub instruction: OutInstruction,
pub balance: Balance,
}
#[derive(
Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Encode, Decode, MaxEncodedLen, TypeInfo,
)]
#[cfg_attr(feature = "std", derive(Zeroize))]
pub enum Destination {
Native(SeraiAddress),
External(OutInstruction),
}
#[test]
fn address() {
use sp_runtime::traits::TrailingZeroInput;
assert_eq!(
FEE_ACCOUNT,
SeraiAddress::decode(&mut TrailingZeroInput::new(b"FeeAccount")).unwrap()
);
}