diff --git a/Cargo.lock b/Cargo.lock index ef7f777d..e4f15e3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3981,7 +3981,7 @@ dependencies = [ "parity-scale-codec", "sails-rs 0.2.1", "scale-info", - "vft-service", + "vft-service 0.1.0 (git+https://github.com/gear-foundation/standards/?branch=gstd-pinned-v1.5.0)", ] [[package]] @@ -15417,6 +15417,18 @@ dependencies = [ "vft-gateway-app", ] +[[package]] +name = "vft-service" +version = "0.1.0" +dependencies = [ + "env_logger 0.9.3", + "gear-core 1.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "gstd 1.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "gtest 1.6.2", + "log", + "sails-rs 0.6.1", +] + [[package]] name = "vft-service" version = "0.1.0" @@ -16697,6 +16709,41 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wrapped-vara" +version = "0.1.0" +dependencies = [ + "gclient 1.6.2", + "gtest 1.6.2", + "sails-client-gen 0.6.1", + "sails-idl-gen 0.6.1", + "sails-rs 0.6.1", + "tokio", + "wrapped-vara", + "wrapped-vara-app", + "wrapped-vara-client", +] + +[[package]] +name = "wrapped-vara-app" +version = "0.1.0" +dependencies = [ + "sails-client-gen 0.6.1", + "sails-rs 0.6.1", + "vft-service 0.1.0", +] + +[[package]] +name = "wrapped-vara-client" +version = "0.1.0" +dependencies = [ + "mockall 0.12.1", + "sails-client-gen 0.6.1", + "sails-idl-gen 0.6.1", + "sails-rs 0.6.1", + "wrapped-vara-app", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index b69a74b3..51e7df55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,10 +20,14 @@ members = [ "gear-programs/vft-treasury/app", "gear-programs/vft-treasury/client", "gear-programs/vft-client", + "gear-programs/vft-service", "gear-programs/*", "gear-programs/checkpoint-light-client/io", "gear-programs/erc20-relay/app", "gear-programs/erc20-relay/client", + "gear-programs/wrapped-vara", + "gear-programs/wrapped-vara/app", + "gear-programs/wrapped-vara/client", "utils-prometheus", "tools/deploy-to-gear", "tools/genesis-config", @@ -67,6 +71,11 @@ erc20-relay = { path = "gear-programs/erc20-relay" } erc20-relay-app = { path = "gear-programs/erc20-relay/app" } erc20-relay-client = { path = "gear-programs/erc20-relay/client" } +wrapped-vara = { path = "gear-programs/wrapped-vara" } +wrapped-vara-app = { path = "gear-programs/wrapped-vara/app" } +wrapped-vara-client = { path = "gear-programs/wrapped-vara/client" } +vft-service = { path = "gear-programs/vft-service" } + # Contracts' deps extended_vft_wasm = { git = "https://github.com/gear-foundation/standards/", branch = "gstd-pinned-v1.5.0" } diff --git a/gear-programs/vft-client/Cargo.toml b/gear-programs/vft-client/Cargo.toml index 13f0f4f6..a0119034 100644 --- a/gear-programs/vft-client/Cargo.toml +++ b/gear-programs/vft-client/Cargo.toml @@ -12,4 +12,4 @@ git-download.workspace = true sails-client-gen.workspace = true [features] -mocks = ["sails-rs/mockall", "dep:mockall"] \ No newline at end of file +mocks = ["sails-rs/mockall", "dep:mockall"] diff --git a/gear-programs/vft-client/extended_vft.idl b/gear-programs/vft-client/extended_vft.idl new file mode 100644 index 00000000..01379404 --- /dev/null +++ b/gear-programs/vft-client/extended_vft.idl @@ -0,0 +1,33 @@ +constructor { + New : (name: str, symbol: str, decimals: u8); +}; + +service Vft { + Burn : (from: actor_id, value: u256) -> bool; + GrantAdminRole : (to: actor_id) -> null; + GrantBurnerRole : (to: actor_id) -> null; + GrantMinterRole : (to: actor_id) -> null; + Mint : (to: actor_id, value: u256) -> bool; + RevokeAdminRole : (from: actor_id) -> null; + RevokeBurnerRole : (from: actor_id) -> null; + RevokeMinterRole : (from: actor_id) -> null; + Approve : (spender: actor_id, value: u256) -> bool; + Transfer : (to: actor_id, value: u256) -> bool; + TransferFrom : (from: actor_id, to: actor_id, value: u256) -> bool; + query Admins : () -> vec actor_id; + query Burners : () -> vec actor_id; + query Minters : () -> vec actor_id; + query Allowance : (owner: actor_id, spender: actor_id) -> u256; + query BalanceOf : (account: actor_id) -> u256; + query Decimals : () -> u8; + query Name : () -> str; + query Symbol : () -> str; + query TotalSupply : () -> u256; + + events { + Minted: struct { to: actor_id, value: u256 }; + Burned: struct { from: actor_id, value: u256 }; + Approval: struct { owner: actor_id, spender: actor_id, value: u256 }; + Transfer: struct { from: actor_id, to: actor_id, value: u256 }; + } +}; diff --git a/gear-programs/vft-service/Cargo.toml b/gear-programs/vft-service/Cargo.toml new file mode 100644 index 00000000..0d0813a5 --- /dev/null +++ b/gear-programs/vft-service/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "vft-service" +version.workspace = true +edition.workspace = true + +[dependencies] +gstd.workspace = true +log = "*" +sails-rs.workspace = true + +[dev-dependencies] +env_logger.workspace = true +gtest.workspace = true +gear-core.workspace = true diff --git a/gear-programs/vft-service/src/funcs.rs b/gear-programs/vft-service/src/funcs.rs new file mode 100644 index 00000000..ee3d7648 --- /dev/null +++ b/gear-programs/vft-service/src/funcs.rs @@ -0,0 +1,623 @@ +use super::utils::{Error, Result, *}; +use sails_rs::prelude::*; + +pub fn allowance(allowances: &AllowancesMap, owner: ActorId, spender: ActorId) -> U256 { + allowances + .get(&(owner, spender)) + .cloned() + .unwrap_or_default() +} + +pub fn approve( + allowances: &mut AllowancesMap, + owner: ActorId, + spender: ActorId, + value: U256, +) -> bool { + if owner == spender { + return false; + } + + let key = (owner, spender); + + if value.is_zero() { + return allowances.remove(&key).is_some(); + } + + let prev = allowances.insert(key, value); + + prev.map(|v| v != value).unwrap_or(true) +} + +pub fn balance_of(balances: &BalancesMap, owner: ActorId) -> U256 { + balances.get(&owner).cloned().unwrap_or_default() +} + +pub fn transfer( + balances: &mut BalancesMap, + from: ActorId, + to: ActorId, + value: U256, +) -> Result { + if from == to || value.is_zero() { + return Ok(false); + } + + let new_from = balance_of(balances, from) + .checked_sub(value) + .ok_or(Error::InsufficientBalance)?; + + let new_to = balance_of(balances, to) + .checked_add(value) + .ok_or(Error::NumericOverflow)?; + + if !new_from.is_zero() { + balances.insert(from, new_from); + } else { + balances.remove(&from); + } + + balances.insert(to, new_to); + + Ok(true) +} + +pub fn transfer_from( + allowances: &mut AllowancesMap, + balances: &mut BalancesMap, + spender: ActorId, + from: ActorId, + to: ActorId, + value: U256, +) -> Result { + if spender == from { + return transfer(balances, from, to, value); + } + + if from == to || value.is_zero() { + return Ok(false); + }; + + let new_allowance = allowance(allowances, from, spender) + .checked_sub(value) + .ok_or(Error::InsufficientAllowance)?; + + let _res = transfer(balances, from, to, value)?; + debug_assert!(_res); + + let key = (from, spender); + + if !new_allowance.is_zero() { + allowances.insert(key, new_allowance); + } else { + allowances.remove(&key); + } + + Ok(true) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::funcs; + use utils::*; + + macro_rules! assert_ok { + ( $x:expr, $y: expr $(,)? ) => {{ + assert_eq!($x.unwrap(), $y); + }}; + } + + macro_rules! assert_err { + ( $x:expr, $y: expr $(,)? ) => {{ + assert_eq!($x.err().expect("Ran into Ok value"), $y); + }}; + } + + #[test] + fn allowance() { + // Initializing thread logger. + let _ = env_logger::try_init(); + + // Creating map with one single approve from Alice to Bob. + let map = allowances_map([(alice(), bob(), U256::exp10(42))]); + + // # Test case #1. + // Approve is returned if exists. + { + assert!(map.contains_key(&(alice(), bob()))); + assert_eq!(funcs::allowance(&map, alice(), bob()), U256::exp10(42)); + } + + // # Test case #2. + // U256::zero() is returned if not exists. + { + assert!(!map.contains_key(&(bob(), alice()))); + assert!(funcs::allowance(&map, bob(), alice()).is_zero()); + } + } + + #[test] + fn approve() { + // Initializing thread logger. + let _ = env_logger::try_init(); + + // Creating empty map. + let mut map = allowances_map([]); + assert!(map.is_empty()); + + // # Test case #1. + // Allowance from Alice to Bob doesn't exist and created. + { + assert!(funcs::approve(&mut map, alice(), bob(), U256::exp10(42))); + assert_eq!(funcs::allowance(&map, alice(), bob()), U256::exp10(42)); + } + + // # Test case #2. + // Allowance from Alice to Bob exist and changed. + { + assert!(funcs::approve(&mut map, alice(), bob(), U256::exp10(24))); + assert_eq!(funcs::allowance(&map, alice(), bob()), U256::exp10(24)); + } + + // # Test case #3. + // Allowance from Alice to Bob exists and not changed. + { + assert!(!funcs::approve(&mut map, alice(), bob(), U256::exp10(24))); + assert_eq!(funcs::allowance(&map, alice(), bob()), U256::exp10(24)); + } + + // # Test case #4. + // Allowance from Alice to Bob exists and removed. + { + assert!(funcs::approve(&mut map, alice(), bob(), U256::zero())); + assert!(funcs::allowance(&map, alice(), bob()).is_zero()); + } + + // # Test case #5. + // Allowance from Alice to Bob doesn't exists and not created. + { + assert!(!funcs::approve(&mut map, alice(), bob(), U256::zero())); + assert!(funcs::allowance(&map, alice(), bob()).is_zero()); + } + + // # Test case #6. + // Allowance is always noop on owner == spender. + { + assert!(!funcs::approve(&mut map, alice(), alice(), U256::exp10(42))); + assert!(funcs::allowance(&map, alice(), alice()).is_zero()); + + assert!(!funcs::approve(&mut map, alice(), alice(), U256::exp10(24))); + assert!(funcs::allowance(&map, alice(), alice()).is_zero()); + + assert!(!funcs::approve(&mut map, alice(), alice(), U256::zero())); + assert!(funcs::allowance(&map, alice(), alice()).is_zero()); + } + } + + #[test] + fn balance_of() { + // Initializing thread logger. + let _ = env_logger::try_init(); + + // Creating map with one single balance belonged to Alice. + let map = balances_map([(alice(), U256::exp10(42))]); + + // # Test case #1. + // Balance is returned if exists. + { + assert!(map.contains_key(&alice())); + assert_eq!(funcs::balance_of(&map, alice()), U256::exp10(42)); + } + + // # Test case #2. + // U256::zero() is returned if not exists. + { + assert!(!map.contains_key(&bob())); + assert!(funcs::balance_of(&map, bob()).is_zero()); + } + } + + #[test] + fn transfer() { + // Initializing thread logger. + let _ = env_logger::try_init(); + + // Creating map with medium balance belonged to Bob and max one to Dave. + let mut map = balances_map([(bob(), U256::exp10(42)), (dave(), U256::MAX)]); + + // # Test case #1. + // Alice transfers to Bob, when Alice has no balance. + { + assert!(funcs::balance_of(&map, alice()).is_zero()); + assert_eq!(funcs::balance_of(&map, bob()), U256::exp10(42)); + + assert_err!( + funcs::transfer(&mut map, alice(), bob(), U256::exp10(20)), + Error::InsufficientBalance + ); + + assert!(funcs::balance_of(&map, alice()).is_zero()); + assert_eq!(funcs::balance_of(&map, bob()), U256::exp10(42)); + } + + // # Test case #2. + // Bob transfers to Alice, when Bob's balance is less than required. + { + assert!(funcs::balance_of(&map, alice()).is_zero()); + assert_eq!(funcs::balance_of(&map, bob()), U256::exp10(42)); + + assert_err!( + funcs::transfer(&mut map, bob(), alice(), U256::exp10(50)), + Error::InsufficientBalance + ); + + assert!(funcs::balance_of(&map, alice()).is_zero()); + assert_eq!(funcs::balance_of(&map, bob()), U256::exp10(42)); + } + + // # Test case #3. + // Dave transfers to Bob, causing numeric overflow. + { + assert_eq!(funcs::balance_of(&map, bob()), U256::exp10(42)); + assert_eq!(funcs::balance_of(&map, dave()), U256::MAX); + + assert_err!( + funcs::transfer(&mut map, dave(), bob(), U256::MAX), + Error::NumericOverflow + ); + + assert_eq!(funcs::balance_of(&map, bob()), U256::exp10(42)); + assert_eq!(funcs::balance_of(&map, dave()), U256::MAX); + } + + // # Test case #4. + // Bob transfers to Alice, when Alice's account doesn't exist. + { + assert!(funcs::balance_of(&map, alice()).is_zero()); + assert_eq!(funcs::balance_of(&map, bob()), U256::exp10(42)); + + assert_ok!( + funcs::transfer(&mut map, bob(), alice(), U256::exp10(10)), + true + ); + + assert_eq!(funcs::balance_of(&map, alice()), U256::exp10(10)); + assert_eq!( + funcs::balance_of(&map, bob()), + U256::exp10(42) - U256::exp10(10) + ); + } + + // # Test case #5. + // Bob transfers to Alice, when Alice's account exists. + { + assert_eq!(funcs::balance_of(&map, alice()), U256::exp10(10)); + assert_eq!( + funcs::balance_of(&map, bob()), + U256::exp10(42) - U256::exp10(10) + ); + + assert_ok!( + funcs::transfer(&mut map, bob(), alice(), U256::exp10(10)), + true + ); + + assert_eq!( + funcs::balance_of(&map, alice()), + U256::exp10(10).saturating_mul(2.into()) + ); + assert_eq!( + funcs::balance_of(&map, bob()), + U256::exp10(42) - U256::exp10(10).saturating_mul(2.into()) + ); + } + + // # Test case #6. + // Bob transfers to Alice, when Alice's account exists and Bob's is removed. + { + assert_eq!( + funcs::balance_of(&map, alice()), + U256::exp10(10).saturating_mul(2.into()) + ); + assert_eq!( + funcs::balance_of(&map, bob()), + U256::exp10(42) - U256::exp10(10).saturating_mul(2.into()) + ); + + assert_ok!( + funcs::transfer( + &mut map, + bob(), + alice(), + U256::exp10(42) - U256::exp10(10).saturating_mul(2.into()) + ), + true + ); + + assert_eq!(funcs::balance_of(&map, alice()), U256::exp10(42)); + assert!(funcs::balance_of(&map, bob()).is_zero()); + } + + // # Test case #7. + // Alice transfers to Charlie, when Alice's account is removed and Charlie's is created. + { + assert_eq!(funcs::balance_of(&map, alice()), U256::exp10(42)); + assert!(funcs::balance_of(&map, charlie()).is_zero()); + + assert_ok!( + funcs::transfer(&mut map, alice(), charlie(), U256::exp10(42)), + true + ); + + assert!(funcs::balance_of(&map, alice()).is_zero()); + assert_eq!(funcs::balance_of(&map, charlie()), U256::exp10(42)); + } + + // # Test case #8. + // Transfer is always noop when from == to. + { + assert!(funcs::balance_of(&map, alice()).is_zero()); + assert_ok!( + funcs::transfer(&mut map, alice(), alice(), U256::exp10(42)), + false + ); + assert!(funcs::balance_of(&map, alice()).is_zero()); + + assert_eq!(funcs::balance_of(&map, charlie()), U256::exp10(42)); + assert_ok!( + funcs::transfer(&mut map, charlie(), charlie(), U256::exp10(42)), + false + ); + assert_eq!(funcs::balance_of(&map, charlie()), U256::exp10(42)); + } + + // # Test case #9. + // Transfer is always noop when value is zero. + { + assert!(funcs::balance_of(&map, alice()).is_zero()); + assert_eq!(funcs::balance_of(&map, charlie()), U256::exp10(42)); + + assert_ok!( + funcs::transfer(&mut map, alice(), charlie(), U256::zero()), + false + ); + assert!(funcs::balance_of(&map, alice()).is_zero()); + assert_eq!(funcs::balance_of(&map, charlie()), U256::exp10(42)); + + assert_ok!( + funcs::transfer(&mut map, charlie(), alice(), U256::zero()), + false + ); + assert!(funcs::balance_of(&map, alice()).is_zero()); + assert_eq!(funcs::balance_of(&map, charlie()), U256::exp10(42)); + } + } + + // Since this uses [`super::transfer`] in underlying impl, it needs only + // check approval specific logic and few transfer's happy cases. + #[test] + fn transfer_from() { + // Initializing thread logger. + let _ = env_logger::try_init(); + + // Creating empty allowances map. + let mut amap = allowances_map([]); + + // Creating balances map with two equal balances belonged to Bob and Dave. + let mut bmap = balances_map([(bob(), U256::exp10(42)), (dave(), U256::exp10(42))]); + + // # Test case #1. + // Bob doesn't need approve to transfer from self to Alice. + // With zero value nothing's changed. + { + assert_ok!( + funcs::transfer_from(&mut amap, &mut bmap, bob(), bob(), alice(), U256::zero()), + false + ); + assert!(funcs::balance_of(&bmap, alice()).is_zero()); + assert_eq!(funcs::balance_of(&bmap, bob()), U256::exp10(42)); + } + + // # Test case #2. + // Bob doesn't need approve to transfer from self to Alice. + { + assert_ok!( + funcs::transfer_from(&mut amap, &mut bmap, bob(), bob(), alice(), U256::exp10(42)), + true + ); + assert_eq!(funcs::balance_of(&bmap, alice()), U256::exp10(42)); + assert!(funcs::balance_of(&bmap, bob()).is_zero()); + } + + // # Test case #3. + // Noop on self transfer with self approve. + { + assert!(funcs::balance_of(&bmap, bob()).is_zero()); + assert_ok!( + funcs::transfer_from(&mut amap, &mut bmap, bob(), bob(), bob(), U256::exp10(42)), + false + ); + assert!(funcs::balance_of(&bmap, bob()).is_zero()); + + assert_eq!(funcs::balance_of(&bmap, alice()), U256::exp10(42)); + assert_ok!( + funcs::transfer_from( + &mut amap, + &mut bmap, + alice(), + alice(), + alice(), + U256::exp10(42) + ), + false + ); + assert_eq!(funcs::balance_of(&bmap, alice()), U256::exp10(42)); + } + + // # Test case #4. + // Bob tries to perform transfer from Alice to Charlie with no approval exists. + { + assert_eq!(funcs::balance_of(&bmap, alice()), U256::exp10(42)); + assert!(funcs::balance_of(&bmap, bob()).is_zero()); + assert!(funcs::balance_of(&bmap, charlie()).is_zero()); + + assert_err!( + funcs::transfer_from( + &mut amap, + &mut bmap, + bob(), + alice(), + charlie(), + U256::exp10(20) + ), + Error::InsufficientAllowance, + ); + + assert_eq!(funcs::balance_of(&bmap, alice()), U256::exp10(42)); + assert!(funcs::balance_of(&bmap, bob()).is_zero()); + assert!(funcs::balance_of(&bmap, charlie()).is_zero()); + } + + // # Test case #5. + // Bob tries to perform transfer from Alice to Charlie with insufficient approval. + { + assert_eq!(funcs::balance_of(&bmap, alice()), U256::exp10(42)); + assert!(funcs::balance_of(&bmap, bob()).is_zero()); + assert!(funcs::balance_of(&bmap, charlie()).is_zero()); + + assert!(funcs::approve(&mut amap, alice(), bob(), U256::exp10(19))); + + assert_err!( + funcs::transfer_from( + &mut amap, + &mut bmap, + bob(), + alice(), + charlie(), + U256::exp10(20) + ), + Error::InsufficientAllowance, + ); + + assert_eq!(funcs::balance_of(&bmap, alice()), U256::exp10(42)); + assert!(funcs::balance_of(&bmap, bob()).is_zero()); + assert!(funcs::balance_of(&bmap, charlie()).is_zero()); + assert_eq!(funcs::allowance(&amap, alice(), bob()), U256::exp10(19)); + } + + // # Test case #6. + // Bob tries to perform transfer from Alice to Charlie with insufficient balance. + { + assert!(funcs::approve(&mut amap, alice(), bob(), U256::MAX)); + + assert_err!( + funcs::transfer_from( + &mut amap, + &mut bmap, + bob(), + alice(), + charlie(), + U256::exp10(43) + ), + Error::InsufficientBalance, + ); + + assert_eq!(funcs::balance_of(&bmap, alice()), U256::exp10(42)); + assert!(funcs::balance_of(&bmap, bob()).is_zero()); + assert!(funcs::balance_of(&bmap, charlie()).is_zero()); + } + + // * `Ok(true)` when allowance is changed + // * `Ok(true)` when allowance is removed + + // # Test case #7. + // Bob performs transfer from Alice to Charlie and allowance is changed. + { + assert_ok!( + funcs::transfer_from( + &mut amap, + &mut bmap, + bob(), + alice(), + charlie(), + U256::exp10(42) + ), + true + ); + + assert!(funcs::balance_of(&bmap, alice()).is_zero()); + assert!(funcs::balance_of(&bmap, bob()).is_zero()); + assert_eq!(funcs::balance_of(&bmap, charlie()), U256::exp10(42)); + assert_eq!( + funcs::allowance(&amap, alice(), bob()), + U256::MAX - U256::exp10(42) + ); + } + + // # Test case #8. + // Alice performs transfer from Charlie to Dave and allowance is removed. + { + assert!(funcs::approve( + &mut amap, + charlie(), + alice(), + U256::exp10(42) + )); + + assert_ok!( + funcs::transfer_from( + &mut amap, + &mut bmap, + alice(), + charlie(), + dave(), + U256::exp10(42) + ), + true + ); + + assert!(funcs::balance_of(&bmap, alice()).is_zero()); + assert!(funcs::balance_of(&bmap, bob()).is_zero()); + assert!(funcs::balance_of(&bmap, charlie()).is_zero()); + assert_eq!( + funcs::balance_of(&bmap, dave()), + U256::exp10(42).saturating_mul(2.into()) + ); + assert!(funcs::allowance(&amap, charlie(), alice()).is_zero()); + } + } + + mod utils { + use super::*; + + pub fn allowances_map( + content: [(ActorId, ActorId, U256); N], + ) -> AllowancesMap { + content + .into_iter() + .map(|(k1, k2, v)| ((k1, k2), v)) + .collect() + } + + pub fn balances_map(content: [(ActorId, U256); N]) -> BalancesMap { + content.into_iter().collect() + } + + pub fn alice() -> ActorId { + 1u64.into() + } + + pub fn bob() -> ActorId { + 2u64.into() + } + + pub fn charlie() -> ActorId { + 3u64.into() + } + + pub fn dave() -> ActorId { + 4u64.into() + } + } +} diff --git a/gear-programs/vft-service/src/lib.rs b/gear-programs/vft-service/src/lib.rs new file mode 100644 index 00000000..738436bb --- /dev/null +++ b/gear-programs/vft-service/src/lib.rs @@ -0,0 +1,167 @@ +#![no_std] +#![allow(clippy::new_without_default)] +use core::fmt::Debug; +use gstd::msg; +use sails_rs::{collections::HashMap, gstd::service, prelude::*}; + +pub mod funcs; +pub mod utils; + +static mut STORAGE: Option = None; + +#[derive(Debug, Default)] +pub struct Storage { + balances: HashMap, + allowances: HashMap<(ActorId, ActorId), U256>, + meta: Metadata, + total_supply: U256, +} + +impl Storage { + pub fn get_mut() -> &'static mut Self { + unsafe { STORAGE.as_mut().expect("Storage is not initialized") } + } + pub fn get() -> &'static Self { + unsafe { STORAGE.as_ref().expect("Storage is not initialized") } + } + pub fn balances() -> &'static mut HashMap { + let storage = unsafe { STORAGE.as_mut().expect("Storage is not initialized") }; + &mut storage.balances + } + pub fn total_supply() -> &'static mut U256 { + let storage = unsafe { STORAGE.as_mut().expect("Storage is not initialized") }; + &mut storage.total_supply + } +} + +#[derive(Debug, Default)] +pub struct Metadata { + pub name: String, + pub symbol: String, + pub decimals: u8, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, TypeInfo)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum Event { + Approval { + owner: ActorId, + spender: ActorId, + value: U256, + }, + Transfer { + from: ActorId, + to: ActorId, + value: U256, + }, +} + +#[derive(Clone)] +pub struct Service(); + +impl Service { + pub fn seed(name: String, symbol: String, decimals: u8) -> Self { + unsafe { + STORAGE = Some(Storage { + meta: Metadata { + name, + symbol, + decimals, + }, + ..Default::default() + }); + } + Self() + } +} + +#[service(events = Event)] +impl Service { + pub fn new() -> Self { + Self() + } + + pub fn approve(&mut self, spender: ActorId, value: U256) -> bool { + let owner = msg::source(); + let storage = Storage::get_mut(); + let mutated = funcs::approve(&mut storage.allowances, owner, spender, value); + + if mutated { + self.notify_on(Event::Approval { + owner, + spender, + value, + }) + .expect("Notification Error"); + } + + mutated + } + + pub fn transfer(&mut self, to: ActorId, value: U256) -> bool { + let from = msg::source(); + let storage = Storage::get_mut(); + let mutated = + utils::panicking(move || funcs::transfer(&mut storage.balances, from, to, value)); + + if mutated { + self.notify_on(Event::Transfer { from, to, value }) + .expect("Notification Error"); + } + + mutated + } + + pub fn transfer_from(&mut self, from: ActorId, to: ActorId, value: U256) -> bool { + let spender = msg::source(); + let storage = Storage::get_mut(); + let mutated = utils::panicking(move || { + funcs::transfer_from( + &mut storage.allowances, + &mut storage.balances, + spender, + from, + to, + value, + ) + }); + + if mutated { + self.notify_on(Event::Transfer { from, to, value }) + .expect("Notification Error"); + } + + mutated + } + + pub fn allowance(&self, owner: ActorId, spender: ActorId) -> U256 { + let storage = Storage::get(); + funcs::allowance(&storage.allowances, owner, spender) + } + + pub fn balance_of(&self, account: ActorId) -> U256 { + let storage = Storage::get(); + funcs::balance_of(&storage.balances, account) + } + + pub fn decimals(&self) -> &'static u8 { + let storage = Storage::get(); + &storage.meta.decimals + } + + pub fn name(&self) -> &'static str { + let storage = Storage::get(); + &storage.meta.name + } + + pub fn symbol(&self) -> &'static str { + let storage = Storage::get(); + &storage.meta.symbol + } + + pub fn total_supply(&self) -> &'static U256 { + let storage = Storage::get(); + &storage.total_supply + } +} diff --git a/gear-programs/vft-service/src/utils.rs b/gear-programs/vft-service/src/utils.rs new file mode 100644 index 00000000..b91dc8ea --- /dev/null +++ b/gear-programs/vft-service/src/utils.rs @@ -0,0 +1,28 @@ +use core::fmt::Debug; +use gstd::{ext, format}; +use sails_rs::{collections::HashMap, prelude::*}; + +pub type AllowancesMap = HashMap<(ActorId, ActorId), U256>; +pub type BalancesMap = HashMap; +pub type Result = core::result::Result; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, TypeInfo)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum Error { + InsufficientAllowance, + InsufficientBalance, + NumericOverflow, + Underflow, +} + +pub fn panicking Result>(f: F) -> T { + match f() { + Ok(v) => v, + Err(e) => panic(e), + } +} + +pub fn panic(err: impl Debug) -> ! { + ext::panic(&format!("{err:?}")) +} diff --git a/gear-programs/wrapped-vara/Cargo.toml b/gear-programs/wrapped-vara/Cargo.toml new file mode 100644 index 00000000..03f2df73 --- /dev/null +++ b/gear-programs/wrapped-vara/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "wrapped-vara" +version.workspace = true +edition.workspace = true + +[dependencies] +wrapped-vara-app.workspace = true + +[build-dependencies] +wrapped-vara-app.workspace = true +sails-rs = { workspace = true, features = ["wasm-builder"] } +sails-client-gen.workspace = true +sails-idl-gen.workspace = true + +[dev-dependencies] +wrapped-vara = { workspace = true, features = ["wasm-binary"] } +wrapped-vara-client.workspace = true +sails-rs = { workspace = true, features = ["gtest", "gclient"] } +tokio = { workspace = true, features = ["rt", "macros"] } +gclient.workspace = true +gtest.workspace = true + +[features] +wasm-binary = [] diff --git a/gear-programs/wrapped-vara/README.md b/gear-programs/wrapped-vara/README.md new file mode 100644 index 00000000..4e10f09f --- /dev/null +++ b/gear-programs/wrapped-vara/README.md @@ -0,0 +1,11 @@ +## The **wrapped-vara** program + +**wrapped-vara** allows you to mint an amount from `value` to an account, and burn and return `value`. + +The program workspace includes the following packages: +- `wrapped-vara` is the package allowing to build WASM binary for the program and IDL file for it. + The package also includes integration tests for the program in the `tests` sub-folder +- `wrapped-vara-app` is the package containing business logic for the program represented by the `TokenizerService` structure. +- `wrapped-vara-client` is the package containing the client for the program allowing to interact with it from another program, tests, or + off-chain client. + diff --git a/gear-programs/wrapped-vara/app/Cargo.toml b/gear-programs/wrapped-vara/app/Cargo.toml new file mode 100644 index 00000000..6c120755 --- /dev/null +++ b/gear-programs/wrapped-vara/app/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "wrapped-vara-app" +version.workspace = true +edition.workspace = true + +[dependencies] +sails-rs.workspace = true +vft-service.workspace = true + +[build-dependencies] +sails-client-gen.workspace = true diff --git a/gear-programs/wrapped-vara/app/src/lib.rs b/gear-programs/wrapped-vara/app/src/lib.rs new file mode 100644 index 00000000..a8236562 --- /dev/null +++ b/gear-programs/wrapped-vara/app/src/lib.rs @@ -0,0 +1,28 @@ +#![no_std] + +mod tokenizer_service; +mod vft_funcs; + +use sails_rs::prelude::*; +use tokenizer_service::TokenizerService; + +pub struct WrappedVaraProgram(()); + +#[sails_rs::program] +impl WrappedVaraProgram { + // Program's constructor + pub fn new(name: String, symbol: String, decimals: u8) -> Self { + vft_service::Service::seed(name, symbol, decimals); + Self(()) + } + + // Exposed tokenizer service + pub fn tokenizer(&self) -> TokenizerService { + TokenizerService::new() + } + + // Exposed vft service + pub fn vft(&self) -> vft_service::Service { + vft_service::Service::new() + } +} diff --git a/gear-programs/wrapped-vara/app/src/tokenizer_service.rs b/gear-programs/wrapped-vara/app/src/tokenizer_service.rs new file mode 100644 index 00000000..5b8d1f89 --- /dev/null +++ b/gear-programs/wrapped-vara/app/src/tokenizer_service.rs @@ -0,0 +1,50 @@ +use crate::vft_funcs; +use sails_rs::{gstd::msg, prelude::*}; + +#[derive(Debug, Encode, Decode, TypeInfo)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum TokenizerEvent { + Minted { to: ActorId, value: u128 }, + Burned { from: ActorId, value: u128 }, +} + +#[derive(Clone, Debug, Default)] +pub struct TokenizerService(()); + +#[sails_rs::service(events = TokenizerEvent)] +impl TokenizerService { + pub fn new() -> Self { + Self(()) + } + + pub async fn mint(&mut self) -> CommandReply { + let value = msg::value(); + if value == 0 { + return CommandReply::new(value); + } + + let to = msg::source(); + if let Err(_err) = vft_funcs::mint(to, value.into()) { + CommandReply::new(0).with_value(value) + } else { + self.notify_on(TokenizerEvent::Minted { to, value }) + .expect("Failed to send `Minted` event"); + CommandReply::new(value) + } + } + + pub async fn burn(&mut self, value: u128) -> CommandReply { + if value == 0 { + return CommandReply::new(value); + } + + let from = msg::source(); + vft_funcs::burn(from, value.into()).expect("Failed to burn value"); + + self.notify_on(TokenizerEvent::Burned { from, value }) + .expect("Failed to send `Burned` event"); + + CommandReply::new(value).with_value(value) + } +} diff --git a/gear-programs/wrapped-vara/app/src/vft_funcs.rs b/gear-programs/wrapped-vara/app/src/vft_funcs.rs new file mode 100644 index 00000000..2cf3b915 --- /dev/null +++ b/gear-programs/wrapped-vara/app/src/vft_funcs.rs @@ -0,0 +1,44 @@ +use sails_rs::prelude::*; +use vft_service::{ + funcs, + utils::{Error, Result}, + Storage, +}; + +pub fn mint(to: ActorId, value: U256) -> Result<()> { + let total_supply = Storage::total_supply(); + let balances = Storage::balances(); + + let new_total_supply = total_supply + .checked_add(value) + .ok_or(Error::NumericOverflow)?; + + let new_to = funcs::balance_of(balances, to) + .checked_add(value) + .ok_or(Error::NumericOverflow)?; + + balances.insert(to, new_to); + *total_supply = new_total_supply; + + Ok(()) +} + +pub fn burn(from: ActorId, value: U256) -> Result<()> { + let total_supply = Storage::total_supply(); + let balances = Storage::balances(); + + let new_total_supply = total_supply.checked_sub(value).ok_or(Error::Underflow)?; + + let new_from = funcs::balance_of(balances, from) + .checked_sub(value) + .ok_or(Error::InsufficientBalance)?; + + if !new_from.is_zero() { + balances.insert(from, new_from); + } else { + balances.remove(&from); + } + + *total_supply = new_total_supply; + Ok(()) +} diff --git a/gear-programs/wrapped-vara/build.rs b/gear-programs/wrapped-vara/build.rs new file mode 100644 index 00000000..e9aa0907 --- /dev/null +++ b/gear-programs/wrapped-vara/build.rs @@ -0,0 +1,23 @@ +use std::{ + env, + fs::File, + io::{BufRead, BufReader}, + path::PathBuf, +}; + +fn main() { + sails_rs::build_wasm(); + + if env::var("__GEAR_WASM_BUILDER_NO_BUILD").is_ok() { + return; + } + + let bin_path_file = File::open(".binpath").unwrap(); + let mut bin_path_reader = BufReader::new(bin_path_file); + let mut bin_path = String::new(); + bin_path_reader.read_line(&mut bin_path).unwrap(); + + let mut idl_path = PathBuf::from(bin_path); + idl_path.set_extension("idl"); + sails_idl_gen::generate_idl_to_file::(idl_path).unwrap(); +} diff --git a/gear-programs/wrapped-vara/client/Cargo.toml b/gear-programs/wrapped-vara/client/Cargo.toml new file mode 100644 index 00000000..bc9188f1 --- /dev/null +++ b/gear-programs/wrapped-vara/client/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "wrapped-vara-client" +version.workspace = true +edition.workspace = true + +[dependencies] +mockall = { workspace = true, optional = true } +sails-rs.workspace = true + +[build-dependencies] +wrapped-vara-app.workspace = true +sails-client-gen.workspace = true +sails-idl-gen.workspace = true + +[features] +mocks = ["sails-rs/mockall", "dep:mockall"] diff --git a/gear-programs/wrapped-vara/client/build.rs b/gear-programs/wrapped-vara/client/build.rs new file mode 100644 index 00000000..fc790ce1 --- /dev/null +++ b/gear-programs/wrapped-vara/client/build.rs @@ -0,0 +1,17 @@ +use sails_client_gen::ClientGenerator; +use std::{env, path::PathBuf}; + +fn main() { + let out_dir_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + let idl_file_path = out_dir_path.join("wrapped_vara.idl"); + + // Generate IDL file for the program + sails_idl_gen::generate_idl_to_file::(&idl_file_path) + .unwrap(); + + // Generate client code from IDL file + ClientGenerator::from_idl_path(&idl_file_path) + .with_mocks("mocks") + .generate_to(PathBuf::from(env::var("OUT_DIR").unwrap()).join("wrapped_vara_client.rs")) + .unwrap(); +} diff --git a/gear-programs/wrapped-vara/client/src/lib.rs b/gear-programs/wrapped-vara/client/src/lib.rs new file mode 100644 index 00000000..a40ed8d3 --- /dev/null +++ b/gear-programs/wrapped-vara/client/src/lib.rs @@ -0,0 +1,4 @@ +#![no_std] + +// Incorporate code generated based on the IDL file +include!(concat!(env!("OUT_DIR"), "/wrapped_vara_client.rs")); diff --git a/gear-programs/wrapped-vara/src/lib.rs b/gear-programs/wrapped-vara/src/lib.rs new file mode 100644 index 00000000..a86ba738 --- /dev/null +++ b/gear-programs/wrapped-vara/src/lib.rs @@ -0,0 +1,14 @@ +#![no_std] + +#[cfg(target_arch = "wasm32")] +pub use wrapped_vara_app::wasm::*; + +#[cfg(feature = "wasm-binary")] +#[cfg(not(target_arch = "wasm32"))] +pub use code::WASM_BINARY_OPT as WASM_BINARY; + +#[cfg(feature = "wasm-binary")] +#[cfg(not(target_arch = "wasm32"))] +mod code { + include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +} diff --git a/gear-programs/wrapped-vara/tests/gclient.rs b/gear-programs/wrapped-vara/tests/gclient.rs new file mode 100644 index 00000000..a4bb1cd7 --- /dev/null +++ b/gear-programs/wrapped-vara/tests/gclient.rs @@ -0,0 +1,169 @@ +use gclient::GearApi; +use sails_rs::{calls::*, gclient::calls::*, prelude::*}; +use wrapped_vara_client::traits::*; + +async fn init_remoting() -> (GearApi, GClientRemoting, CodeId) { + let gear_path = option_env!("GEAR_PATH"); + if gear_path.is_none() { + crate::panic!("the 'GEAR_PATH' environment variable was not set during compile time"); + } + let api = GearApi::dev_from_path(gear_path.unwrap()).await.unwrap(); + let (code_id, ..) = api.upload_code(wrapped_vara::WASM_BINARY).await.unwrap(); + + let remoting = GClientRemoting::new(api.clone()); + (api, remoting, code_id) +} + +#[tokio::test] +#[ignore = "requires run gear node on GEAR_PATH"] +async fn factory_works() { + // arrange + let (_api, remoting, program_code_id) = init_remoting().await; + + // act + let program_factory = wrapped_vara_client::WrappedVaraFactory::new(remoting.clone()); + let program_id = program_factory + .new("Name".into(), "Symbol".into(), 10u8) + .send_recv(program_code_id, b"salt") + .await + .unwrap(); + + let vft_client = wrapped_vara_client::Vft::new(remoting.clone()); + let total_supply = vft_client + .total_supply() + .recv(program_id) + .await + .expect("Failed"); + + // assert + assert!(total_supply.is_zero()); +} + +#[tokio::test] +#[ignore = "requires run gear node on GEAR_PATH"] +async fn mint_from_value_works() { + // arrange + let (api, remoting, program_code_id) = init_remoting().await; + let admin_id = + ActorId::try_from(api.account_id().encode().as_ref()).expect("failed to create actor id"); + + let program_factory = wrapped_vara_client::WrappedVaraFactory::new(remoting.clone()); + + let program_id = program_factory + .new("Name".into(), "Symbol".into(), 10u8) + .send_recv(program_code_id, b"salt") + .await + .unwrap(); + + let initial_balance = api + .free_balance(admin_id) + .await + .expect("Failed to get free balance"); + let program_initial_balance = api + .free_balance(program_id) + .await + .expect("Failed to get free balance"); + dbg!(initial_balance, program_initial_balance); + + let mint_value = 10_000_000_000_000; + + let mut client = wrapped_vara_client::Tokenizer::new(remoting.clone()); + + // act + client + .mint() + .with_value(mint_value) + .send_recv(program_id) + .await + .expect("Failed send_recv"); + + // assert + let balance = api + .free_balance(admin_id) + .await + .expect("Failed to get free balance"); + let program_balance = api + .free_balance(program_id) + .await + .expect("Failed to get free balance"); + dbg!(balance, program_balance); + + assert_eq!(program_balance, mint_value + program_initial_balance); +} + +#[tokio::test] +#[ignore = "requires run gear node on GEAR_PATH"] +async fn burn_and_return_value_works() { + let (api, remoting, program_code_id) = init_remoting().await; + let admin_id = + ActorId::try_from(api.account_id().encode().as_ref()).expect("failed to create actor id"); + + let program_factory = wrapped_vara_client::WrappedVaraFactory::new(remoting.clone()); + + let program_id = program_factory + .new("Name".into(), "Symbol".into(), 10u8) + .send_recv(program_code_id, b"salt") + .await + .unwrap(); + + let program_initial_balance = api + .free_balance(program_id) + .await + .expect("Failed to get free balance"); + + let mint_value = 10_000_000_000_000; + + let mut client = wrapped_vara_client::Tokenizer::new(remoting.clone()); + let vft_client = wrapped_vara_client::Vft::new(remoting.clone()); + + client + .mint() + .with_value(mint_value) + .send_recv(program_id) + .await + .expect("Failed send_recv"); + + let client_balance = vft_client + .balance_of(admin_id) + .recv(program_id) + .await + .unwrap(); + + let balance = api + .free_balance(admin_id) + .await + .expect("Failed to get free balance"); + let program_balance = api + .free_balance(program_id) + .await + .expect("Failed to get free balance"); + dbg!(balance, program_balance, client_balance); + assert_eq!(program_balance, mint_value + program_initial_balance); + assert_eq!(client_balance, mint_value.into()); + + client + .burn(mint_value) + .send_recv(program_id) + .await + .expect("Failed send_recv"); + + let client_balance = vft_client + .balance_of(admin_id) + .recv(program_id) + .await + .unwrap(); + + let balance = api + .free_balance(admin_id) + .await + .expect("Failed to get free balance"); + let program_balance = api + .free_balance(program_id) + .await + .expect("Failed to get free balance"); + + // assert + dbg!(balance, program_balance, client_balance); + assert_eq!(program_balance, program_initial_balance); + assert!(client_balance.is_zero()); +} diff --git a/gear-programs/wrapped-vara/tests/gtest.rs b/gear-programs/wrapped-vara/tests/gtest.rs new file mode 100644 index 00000000..976ec0de --- /dev/null +++ b/gear-programs/wrapped-vara/tests/gtest.rs @@ -0,0 +1,134 @@ +use gtest::System; +use sails_rs::{calls::*, gtest::calls::*, prelude::*}; +use wrapped_vara_client::traits::*; + +pub const ADMIN_ID: u64 = 42; + +pub fn init_remoting() -> (GTestRemoting, CodeId) { + let system = System::new(); + system.init_logger(); + system.mint_to(ADMIN_ID, 100_000_000_000_000); + let remoting = GTestRemoting::new(system, ADMIN_ID.into()); + + // Submit program code into the system + let program_code_id = remoting.system().submit_code(wrapped_vara::WASM_BINARY); + (remoting, program_code_id) +} + +#[tokio::test] +async fn factory_works() { + let (remoting, program_code_id) = init_remoting(); + + let program_factory = wrapped_vara_client::WrappedVaraFactory::new(remoting.clone()); + + let program_id = program_factory + .new("Name".into(), "Symbol".into(), 10u8) + .send_recv(program_code_id, b"salt") + .await + .unwrap(); + + let vft_client = wrapped_vara_client::Vft::new(remoting.clone()); + + let total_supply = vft_client + .total_supply() + .recv(program_id) + .await + .expect("Failed"); + + // assert + assert!(total_supply.is_zero()); +} + +#[tokio::test] +async fn mint_from_value_works() { + let (remoting, program_code_id) = init_remoting(); + + let program_factory = wrapped_vara_client::WrappedVaraFactory::new(remoting.clone()); + + let program_id = program_factory + .new("Name".into(), "Symbol".into(), 10u8) + .send_recv(program_code_id, b"salt") + .await + .unwrap(); + + let initial_balance = remoting.system().balance_of(ADMIN_ID); + let mint_value = 10_000_000_000_000; + + let program_initial_balance = remoting.system().balance_of(program_id); + + let mut client = wrapped_vara_client::Tokenizer::new(remoting.clone()); + + let minted_value = client + .mint() + .with_value(mint_value) + .send_recv(program_id) + .await + .expect("Failed send_recv"); + + assert_eq!(mint_value, minted_value); + + let balance = remoting.system().balance_of(ADMIN_ID); + let program_balance = remoting.system().balance_of(program_id); + + assert!(balance < initial_balance - mint_value); + assert_eq!(program_balance, mint_value + program_initial_balance); +} + +#[tokio::test] +async fn burn_and_return_value_works() { + let (remoting, program_code_id) = init_remoting(); + + let program_factory = wrapped_vara_client::WrappedVaraFactory::new(remoting.clone()); + + let program_id = program_factory + .new("Name".into(), "Symbol".into(), 10u8) + .send_recv(program_code_id, b"salt") + .await + .unwrap(); + + let initial_balance = remoting.system().balance_of(ADMIN_ID); + let mint_value = 10_000_000_000_000; + + let program_initial_balance = remoting.system().balance_of(program_id); + + let mut client = wrapped_vara_client::Tokenizer::new(remoting.clone()); + let vft_client = wrapped_vara_client::Vft::new(remoting.clone()); + + client + .mint() + .with_value(mint_value) + .send_recv(program_id) + .await + .expect("Failed send_recv"); + + let client_balance = vft_client + .balance_of(ADMIN_ID.into()) + .recv(program_id) + .await + .unwrap(); + + let balance = remoting.system().balance_of(ADMIN_ID); + let program_balance = remoting.system().balance_of(program_id); + assert!(balance < initial_balance - mint_value); + assert_eq!(program_balance, mint_value + program_initial_balance); + assert_eq!(client_balance, mint_value.into()); + + client + .burn(mint_value) + .send_recv(program_id) + .await + .expect("Failed send_recv"); + + let client_balance = vft_client + .balance_of(ADMIN_ID.into()) + .recv(program_id) + .await + .unwrap(); + + let balance = remoting.system().balance_of(ADMIN_ID); + let program_balance = remoting.system().balance_of(program_id); + + assert!(client_balance.is_zero()); + assert!(balance > initial_balance - mint_value); + assert_eq!(program_balance, program_initial_balance); +}