From 6f5714bd4efeb9e662624b69db364ceab2021001 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Mon, 4 Jul 2022 12:52:48 +1000 Subject: [PATCH 01/21] tidy fvm_dispatch --- Cargo.toml | 5 +++++ fvm_dispatch/src/hash.rs | 3 +++ fvm_dispatch/src/message.rs | 2 ++ 3 files changed, 10 insertions(+) create mode 100644 Cargo.toml diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..0d163476 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] + +members = [ + "fvm_dispatch" +] \ No newline at end of file diff --git a/fvm_dispatch/src/hash.rs b/fvm_dispatch/src/hash.rs index 51a69c6e..673cdaee 100644 --- a/fvm_dispatch/src/hash.rs +++ b/fvm_dispatch/src/hash.rs @@ -26,10 +26,13 @@ pub struct MethodHasher { impl MethodHasher { const CONSTRUCTOR_METHOD_NAME: &'static str = "Constructor"; const CONSTRUCTOR_METHOD_NUMBER: u64 = 1_u64; + + /// Create a new MethodHasher using the given hash algorithm pub fn new(hasher: T) -> Self { Self { hasher } } + /// Generate the conventional method number based off an exported name pub fn method_number(&self, method_name: &str) -> u64 { if method_name == Self::CONSTRUCTOR_METHOD_NAME { Self::CONSTRUCTOR_METHOD_NUMBER diff --git a/fvm_dispatch/src/message.rs b/fvm_dispatch/src/message.rs index adbfff62..8af7bc43 100644 --- a/fvm_dispatch/src/message.rs +++ b/fvm_dispatch/src/message.rs @@ -10,12 +10,14 @@ pub struct MethodDispatcher { } impl MethodDispatcher { + /// Create a new MethodDispatcher with a given hasher pub fn new(hasher: T) -> Self { Self { method_hasher: MethodHasher::new(hasher), } } + /// Call a method on another actor by conventional name pub fn call_method( &self, to: &Address, From 4eb159fc93cac361782b55ee8569f6fc374babc4 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Mon, 4 Jul 2022 13:52:31 +1000 Subject: [PATCH 02/21] define token interfaces --- Cargo.toml | 3 ++- fil_token/Cargo.toml | 9 +++++++++ fil_token/src/lib.rs | 4 ++++ fil_token/src/token/mod.rs | 30 ++++++++++++++++++++++++++++++ fil_token/src/token/receiver.rs | 10 ++++++++++ 5 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 fil_token/Cargo.toml create mode 100644 fil_token/src/lib.rs create mode 100644 fil_token/src/token/mod.rs create mode 100644 fil_token/src/token/receiver.rs diff --git a/Cargo.toml b/Cargo.toml index 0d163476..2596670d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ - "fvm_dispatch" + "fvm_dispatch", + "fil_token", ] \ No newline at end of file diff --git a/fil_token/Cargo.toml b/fil_token/Cargo.toml new file mode 100644 index 00000000..f29fd6ad --- /dev/null +++ b/fil_token/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "fil_token" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +fvm_shared = { version = "0.8.0" } \ No newline at end of file diff --git a/fil_token/src/lib.rs b/fil_token/src/lib.rs new file mode 100644 index 00000000..67619c6c --- /dev/null +++ b/fil_token/src/lib.rs @@ -0,0 +1,4 @@ +pub mod token; + +#[cfg(test)] +mod tests {} diff --git a/fil_token/src/token/mod.rs b/fil_token/src/token/mod.rs new file mode 100644 index 00000000..35bb473f --- /dev/null +++ b/fil_token/src/token/mod.rs @@ -0,0 +1,30 @@ +pub mod receiver; + +use fvm_shared::address::Address; +pub use fvm_shared::econ::TokenAmount; + +pub type Result = std::result::Result; +pub enum TokenError {} + +/// A standard fungible token interface allowing for on-chain transactions +pub trait Token { + fn name() -> String; + + fn symbol() -> String; + + fn total_supply() -> TokenAmount; + + fn balance_of(holder: Address) -> Result; + + fn increase_allowance(spender: Address, value: TokenAmount) -> Result; + + fn decrease_allowance(spender: Address, value: TokenAmount) -> Result; + + fn revoke_allowance(spender: Address) -> Result<()>; + + fn allowance(owner: Address, spender: Address) -> Result; + + fn burn(amount: TokenAmount, data: &[u8]) -> Result; + + fn burn_from(from: Address, amount: TokenAmount, data: &[u8]) -> Result; +} diff --git a/fil_token/src/token/receiver.rs b/fil_token/src/token/receiver.rs new file mode 100644 index 00000000..cf27518b --- /dev/null +++ b/fil_token/src/token/receiver.rs @@ -0,0 +1,10 @@ +use fvm_shared::{address::Address, econ::TokenAmount}; + +pub type Result = std::result::Result; + +pub enum ReceiverError {} + +/// Standard interface for a contract that wishes to receive tokens +pub trait TokenReceiver { + fn token_received(from: Address, amount: TokenAmount, data: &[u8]) -> Result; +} From b14b584abccf565a368361f08b00c572a7815e83 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Mon, 4 Jul 2022 16:43:36 +1000 Subject: [PATCH 03/21] basic token functionality --- Cargo.toml | 2 + fil_token/Cargo.toml | 11 +- fil_token/src/blockstore.rs | 40 ++ fil_token/src/lib.rs | 1 + fil_token/src/token/mod.rs | 534 +++++++++++++++++- testing/integration/Cargo.toml | 4 + .../actors/fil_token_actor/Cargo.toml | 12 + .../actors/fil_token_actor/build.rs | 12 + .../actors/fil_token_actor/src/lib.rs | 11 + testing/integration/src/lib.rs | 1 + 10 files changed, 616 insertions(+), 12 deletions(-) create mode 100644 fil_token/src/blockstore.rs create mode 100644 testing/integration/Cargo.toml create mode 100644 testing/integration/actors/fil_token_actor/Cargo.toml create mode 100644 testing/integration/actors/fil_token_actor/build.rs create mode 100644 testing/integration/actors/fil_token_actor/src/lib.rs create mode 100644 testing/integration/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 2596670d..07001f69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,4 +3,6 @@ members = [ "fvm_dispatch", "fil_token", + "testing/integration", + "testing/integration/actors/fil_token_actor", ] \ No newline at end of file diff --git a/fil_token/Cargo.toml b/fil_token/Cargo.toml index f29fd6ad..37997649 100644 --- a/fil_token/Cargo.toml +++ b/fil_token/Cargo.toml @@ -6,4 +6,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fvm_shared = { version = "0.8.0" } \ No newline at end of file +anyhow = "1.0.56" +cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } +fvm_dispatch = { version = "0.1.0", path = "../fvm_dispatch" } +fvm_ipld_blockstore = "0.1.1" +fvm_ipld_hamt = "0.5.1" +fvm_ipld_amt = { version = "0.4.2", features = ["go-interop"] } +fvm_ipld_encoding = "0.2.2" +fvm_sdk = { version = "1.0.0" } +fvm_shared = { version = "0.8.0" } +serde = { version = "1.0.136", features = ["derive"] } \ No newline at end of file diff --git a/fil_token/src/blockstore.rs b/fil_token/src/blockstore.rs new file mode 100644 index 00000000..0de47843 --- /dev/null +++ b/fil_token/src/blockstore.rs @@ -0,0 +1,40 @@ +use std::convert::TryFrom; + +use anyhow::{anyhow, Result}; +use cid::multihash::Code; +use cid::Cid; +use fvm_ipld_blockstore::Block; +use fvm_sdk::ipld; + +/// A blockstore that delegates to IPLD syscalls. +#[derive(Default, Debug, Copy, Clone)] +pub struct Blockstore; + +impl fvm_ipld_blockstore::Blockstore for Blockstore { + fn get(&self, cid: &Cid) -> Result>> { + // If this fails, the _CID_ is invalid. I.e., we have a bug. + ipld::get(cid) + .map(Some) + .map_err(|e| anyhow!("get failed with {:?} on CID '{}'", e, cid)) + } + + fn put_keyed(&self, k: &Cid, block: &[u8]) -> Result<()> { + let code = Code::try_from(k.hash().code()).map_err(|e| anyhow!(e.to_string()))?; + let k2 = self.put(code, &Block::new(k.codec(), block))?; + if k != &k2 { + return Err(anyhow!("put block with cid {} but has cid {}", k, k2)); + } + Ok(()) + } + fn put(&self, code: Code, block: &Block) -> Result + where + D: AsRef<[u8]>, + { + // TODO: Don't hard-code the size. Unfortunately, there's no good way to get it from the + // codec at the moment. + const SIZE: u32 = 32; + let k = ipld::put(code.into(), SIZE, block.codec, block.data.as_ref()) + .map_err(|e| anyhow!("put failed with {:?}", e))?; + Ok(k) + } +} diff --git a/fil_token/src/lib.rs b/fil_token/src/lib.rs index 67619c6c..a6309255 100644 --- a/fil_token/src/lib.rs +++ b/fil_token/src/lib.rs @@ -1,3 +1,4 @@ +pub mod blockstore; pub mod token; #[cfg(test)] diff --git a/fil_token/src/token/mod.rs b/fil_token/src/token/mod.rs index 35bb473f..dc25f2db 100644 --- a/fil_token/src/token/mod.rs +++ b/fil_token/src/token/mod.rs @@ -1,30 +1,542 @@ pub mod receiver; +use std::ops::Add; +use std::ops::Sub; + +use anyhow::anyhow; +use cid::Cid; + +use cid::multihash::Code; + +use fvm_ipld_blockstore::Blockstore as Store; +use fvm_ipld_encoding::tuple::*; +use fvm_ipld_encoding::Cbor; +use fvm_ipld_encoding::CborStore; +use fvm_ipld_encoding::DAG_CBOR; +use fvm_ipld_hamt::Hamt; + use fvm_shared::address::Address; +use fvm_shared::bigint::bigint_ser; +use fvm_shared::bigint::bigint_ser::BigIntDe; +use fvm_shared::bigint::BigInt; +use fvm_shared::bigint::Zero; pub use fvm_shared::econ::TokenAmount; +use fvm_shared::ActorID; +use fvm_shared::HAMT_BIT_WIDTH; + +use fvm_sdk::ipld; +use fvm_sdk::sself; + +use crate::blockstore::Blockstore; pub type Result = std::result::Result; -pub enum TokenError {} + +type TransferResult = std::result::Result; + +pub struct TokenAmountDiff { + pub required: TokenAmount, + pub actual: TokenAmount, +} + +pub enum TransferError { + NoRecrHook, + InsufficientAllowance(TokenAmountDiff), + InsufficientBalance(TokenAmountDiff), +} + +pub enum TokenError { + AddrNotFound(Address), + Arithmetic, + Ipld(fvm_ipld_hamt::Error), + Err(anyhow::Error), + Transfer(TransferError), +} + +impl From for TokenError { + fn from(e: anyhow::Error) -> Self { + Self::Err(e) + } +} + +impl From for TokenError { + fn from(e: fvm_ipld_hamt::Error) -> Self { + Self::Ipld(e) + } +} + +impl From for TokenError { + fn from(e: TransferError) -> Self { + Self::Transfer(e) + } +} + +/// A macro to abort concisely. +macro_rules! abort { + ($code:ident, $msg:literal $(, $ex:expr)*) => { + fvm_sdk::vm::abort( + fvm_shared::error::ExitCode::$code.value(), + Some(format!($msg, $($ex,)*).as_str()), + ) + }; +} /// A standard fungible token interface allowing for on-chain transactions pub trait Token { - fn name() -> String; + fn name(&self) -> String; + + fn symbol(&self) -> String; + + fn total_supply(&self) -> TokenAmount; + + /// Mint a number of tokens and assign them to a specific Actor + /// + /// Minting can only be done in the constructor as once off + /// TODO: allow authorised actors to mint more supply + fn mint( + &self, + amount: TokenAmount, + initial_holder: Address, + bs: &Blockstore, + ) -> Result; + + /// Gets the balance of a particular address (if it exists). + fn balance_of(&self, holder: Address, bs: &Blockstore) -> Result; + + /// Atomically increase the amount that a spender can pull from an account + fn increase_allowance( + &self, + spender: Address, + value: TokenAmount, + bs: &Blockstore, + ) -> Result; + + /// Atomically decrease the amount that a spender can pull from an account + /// + /// The allowance cannot go below 0 and will be capped if the requested decrease + /// is more than the current allowance + fn decrease_allowance( + &self, + spender: Address, + value: TokenAmount, + bs: &Blockstore, + ) -> Result; + + fn revoke_allowance(&self, spender: Address, bs: &Blockstore) -> Result<()>; + + fn allowance(&self, owner: Address, spender: Address, bs: &Blockstore) -> Result; + + fn burn(&self, amount: TokenAmount, data: &[u8], bs: &Blockstore) -> Result; + + fn transfer_from( + &self, + owner: Address, + spender: Address, + amount: TokenAmount, + bs: &Blockstore, + ) -> Result; + + fn burn_from( + &self, + from: Address, + amount: TokenAmount, + data: &[u8], + bs: &Blockstore, + ) -> Result; +} + +/// Token state ipld structure +#[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] +pub struct DefaultToken { + #[serde(with = "bigint_ser")] + supply: TokenAmount, + name: String, + symbol: String, + + balances: Cid, + allowances: Cid, +} + +/// Default token implementation +impl DefaultToken { + pub fn new(name: &str, symbol: &str, store: &BS) -> anyhow::Result + where + BS: Store, + { + let empty_balance_map = Hamt::<_, ()>::new_with_bit_width(store, HAMT_BIT_WIDTH) + .flush() + .map_err(|e| anyhow!("Failed to create empty balances map state {}", e))?; + let empty_allowances_map = Hamt::<_, ()>::new_with_bit_width(store, HAMT_BIT_WIDTH) + .flush() + .map_err(|e| anyhow!("Failed to create empty balances map state {}", e))?; + + Ok(Self { + supply: Default::default(), + name: name.to_string(), + symbol: symbol.to_string(), + balances: empty_balance_map, + allowances: empty_allowances_map, + }) + } + + pub fn load() -> Self { + // First, load the current state root. + let root = match sself::root() { + Ok(root) => root, + Err(err) => abort!(USR_ILLEGAL_STATE, "failed to get root: {:?}", err), + }; + + // Load the actor state from the state tree. + match Blockstore.get_cbor::(&root) { + Ok(Some(state)) => state, + Ok(None) => abort!(USR_ILLEGAL_STATE, "state does not exist"), + Err(err) => abort!(USR_ILLEGAL_STATE, "failed to get state: {}", err), + } + } + + pub fn save(&self) -> Cid { + let serialized = match fvm_ipld_encoding::to_vec(self) { + Ok(s) => s, + Err(err) => abort!(USR_SERIALIZATION, "failed to serialize state: {:?}", err), + }; + let cid = match ipld::put(Code::Blake2b256.into(), 32, DAG_CBOR, serialized.as_slice()) { + Ok(cid) => cid, + Err(err) => abort!(USR_SERIALIZATION, "failed to store initial state: {:}", err), + }; + if let Err(err) = sself::set_root(&cid) { + abort!(USR_ILLEGAL_STATE, "failed to set root ciid: {:}", err); + } + cid + } + + fn get_balance_map(&self, bs: &Blockstore) -> Hamt { + let balances = match Hamt::::load(&self.balances, *bs) { + Ok(map) => map, + Err(err) => abort!(USR_ILLEGAL_STATE, "Failed to load balances hamt: {:?}", err), + }; + balances + } + + /// Get the global allowances map + /// + /// Gets a HAMT with CIDs linking to other HAMTs + fn get_allowances_map(&self, bs: &Blockstore) -> Hamt { + let allowances = match Hamt::::load(&self.allowances, *bs) { + Ok(map) => map, + Err(err) => abort!( + USR_ILLEGAL_STATE, + "Failed to load allowances hamt: {:?}", + err + ), + }; + allowances + } + + /// Get the allowances map of a specific actor, lazily creating one if it didn't exist + fn get_actor_allowance_map( + &self, + bs: &Blockstore, + authoriser: ActorID, + ) -> Hamt { + let mut global_allowances = self.get_allowances_map(bs); + match global_allowances.get(&authoriser) { + Ok(Some(map)) => { + // authorising actor already had an allowance map, return it + Hamt::::load(map, *bs).unwrap() + } + Ok(None) => { + // authorising actor does not have an allowance map, create one and return it + let mut new_actor_allowances = Hamt::new(*bs); + let cid = new_actor_allowances + .flush() + .map_err(|e| anyhow!("Failed to create empty balances map state {}", e)) + .unwrap(); + global_allowances.set(authoriser, cid).unwrap(); + new_actor_allowances + } + Err(e) => abort!( + USR_ILLEGAL_STATE, + "failed to get actor's allowance map {:?}", + e + ), + } + } + + fn enough_allowance( + &self, + from: ActorID, + spender: ActorID, + to: ActorID, + amount: &TokenAmount, + bs: &Blockstore, + ) -> std::result::Result<(), TokenAmountDiff> { + if spender == from { + return std::result::Result::Ok(()); + } + + let allowances = self.get_actor_allowance_map(bs, from); + let allowance = match allowances.get(&to) { + Ok(Some(amount)) => amount.0.clone(), + _ => TokenAmount::zero(), + }; + + if allowance.lt(&amount) { + Err(TokenAmountDiff { + actual: allowance, + required: amount.clone(), + }) + } else { + std::result::Result::Ok(()) + } + } + + fn enough_balance( + &self, + from: ActorID, + amount: &TokenAmount, + bs: &Blockstore, + ) -> std::result::Result<(), TokenAmountDiff> { + let balances = self.get_balance_map(bs); + let balance = match balances.get(&from) { + Ok(Some(amount)) => amount.0.clone(), + _ => TokenAmount::zero(), + }; + + if balance.lt(&amount) { + Err(TokenAmountDiff { + actual: balance, + required: amount.clone(), + }) + } else { + std::result::Result::Ok(()) + } + } + + /// Atomically make a transfer + fn make_transfer( + &self, + bs: &Blockstore, + amount: &TokenAmount, + from: ActorID, + spender: ActorID, + to: ActorID, + ) -> TransferResult { + if let Err(e) = self.enough_allowance(from, spender, to, amount, bs) { + return Err(TransferError::InsufficientAllowance(e)); + } + if let Err(e) = self.enough_balance(from, amount, bs) { + return Err(TransferError::InsufficientBalance(e)); + } + + // Decrease allowance, decrease balance + // From the above checks, we know these exist + // TODO: do this in a transaction to avoid re-entrancy bugs + let mut allowances = self.get_actor_allowance_map(bs, from); + let allowance = allowances.get(&to).unwrap().unwrap(); + let new_allowance = allowance.0.clone().sub(amount); + allowances.set(to, BigIntDe(new_allowance)).unwrap(); + + let mut balances = self.get_balance_map(bs); + let sender_balance = balances.get(&from).unwrap().unwrap(); + let new_sender_balance = sender_balance.0.clone().sub(amount); + balances.set(from, BigIntDe(new_sender_balance)).unwrap(); + + // TODO: call the receive hook + + // TODO: if no hook, revert the balance and allowance change + + // if successful, mark the balance as having been credited + + let receiver_balance = balances.get(&to).unwrap().unwrap(); + let new_receiver_balance = receiver_balance.0.clone().add(amount); + balances.set(to, BigIntDe(new_receiver_balance)).unwrap(); + + Ok(amount.clone()) + } +} + +fn resolve_address(address: &Address) -> Result { + match fvm_sdk::actor::resolve_address(address) { + Some(addr) => Ok(addr), + None => Err(TokenError::AddrNotFound(*address)), + } +} + +impl Cbor for DefaultToken {} + +impl Token for DefaultToken { + fn name(&self) -> String { + let state = Self::load(); + state.name + } + + fn symbol(&self) -> String { + let state = Self::load(); + state.symbol + } + + fn total_supply(&self) -> TokenAmount { + let state = Self::load(); + state.supply + } + + fn mint(&self, amount: TokenAmount, treasury: Address, bs: &Blockstore) -> Result { + // TODO: check we are being called in the constructor by init system actor + + let mut state = Self::load(); + let mut balances = self.get_balance_map(bs); + + let treasury = match fvm_sdk::actor::resolve_address(&treasury) { + Some(id) => id, + None => return Err(TokenError::AddrNotFound(treasury)), + }; + + // Mint the tokens into a specified account + balances.set(treasury, BigIntDe(amount.clone()))?; + + // set the global supply of the contract + state.supply = amount.clone(); + + Ok(amount) + } + + fn balance_of(&self, holder: Address, bs: &Blockstore) -> Result { + // Load the HAMT holding balances + let balances = self.get_balance_map(bs); + + // Resolve the address + let addr_id = match fvm_sdk::actor::resolve_address(&holder) { + Some(id) => id, + None => return Err(TokenError::AddrNotFound(holder)), + }; + + match balances.get(&addr_id) { + Ok(Some(bal)) => Ok(bal.clone().0), + Ok(None) => Ok(TokenAmount::zero()), + Err(err) => abort!( + USR_ILLEGAL_STATE, + "Failed to get balance from hamt: {:?}", + err + ), + } + } + + fn increase_allowance( + &self, + spender: Address, + value: TokenAmount, + bs: &Blockstore, + ) -> Result { + let caller_id = fvm_sdk::message::caller(); + + let caller_allowances_map = self.get_actor_allowance_map(bs, caller_id); + + let spender = match fvm_sdk::actor::resolve_address(&spender) { + Some(id) => id, + None => return Err(TokenError::AddrNotFound(spender)), + }; + + let new_amount = match caller_allowances_map.get(&spender)? { + Some(existing_allowance) => existing_allowance.0.checked_add(&value), + None => todo!(), + }; + + match new_amount { + Some(amount) => Ok(amount), + None => Err(TokenError::Arithmetic), + } + } + + fn decrease_allowance( + &self, + spender: Address, + value: TokenAmount, + bs: &Blockstore, + ) -> Result { + let caller_id = fvm_sdk::message::caller(); + + // TODO: can exit earlier if the authorisers map doesn't even exist + let mut caller_allowances_map = self.get_actor_allowance_map(bs, caller_id); + + let spender = match fvm_sdk::actor::resolve_address(&spender) { + Some(id) => id, + None => return Err(TokenError::AddrNotFound(spender)), + }; - fn symbol() -> String; + // check that the allowance is larger than the decrease + let existing_balance = match caller_allowances_map.get(&spender)? { + Some(existing_allowance) => { + if existing_allowance.0.gt(&value) { + Some(existing_allowance.clone().0) + } else { + None + } + } + None => None, + }; - fn total_supply() -> TokenAmount; + match existing_balance { + Some(existing_balance) => { + let new_allowance = existing_balance.sub(value); + caller_allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; + Ok(new_allowance) + } + _ => { + caller_allowances_map.set(spender, BigIntDe(BigInt::zero()))?; + Ok(BigInt::zero()) + } + } + } - fn balance_of(holder: Address) -> Result; + fn revoke_allowance(&self, _spender: Address, _bs: &Blockstore) -> Result<()> { + todo!() + } - fn increase_allowance(spender: Address, value: TokenAmount) -> Result; + fn allowance(&self, owner: Address, spender: Address, bs: &Blockstore) -> Result { + let owner = match fvm_sdk::actor::resolve_address(&owner) { + Some(id) => id, + None => return Err(TokenError::AddrNotFound(owner)), + }; + let spender = match fvm_sdk::actor::resolve_address(&spender) { + Some(id) => id, + None => return Err(TokenError::AddrNotFound(spender)), + }; - fn decrease_allowance(spender: Address, value: TokenAmount) -> Result; + let allowance_map = self.get_actor_allowance_map(bs, owner); + match allowance_map.get(&spender)? { + Some(allowance) => Ok(allowance.0.clone()), + None => Ok(TokenAmount::zero()), + } + } - fn revoke_allowance(spender: Address) -> Result<()>; + fn burn(&self, _amount: TokenAmount, _data: &[u8], _bs: &Blockstore) -> Result { + todo!() + } - fn allowance(owner: Address, spender: Address) -> Result; + fn transfer_from( + &self, + owner: Address, + receiver: Address, + amount: TokenAmount, + bs: &Blockstore, + ) -> Result { + let spender = fvm_sdk::message::caller(); + let owner = resolve_address(&owner)?; + let receiver = resolve_address(&receiver)?; - fn burn(amount: TokenAmount, data: &[u8]) -> Result; + let res = self.make_transfer(bs, &amount, owner, spender, receiver); + match res { + Ok(amount) => Ok(amount), + Err(e) => Err(e.into()), + } + } - fn burn_from(from: Address, amount: TokenAmount, data: &[u8]) -> Result; + fn burn_from( + &self, + _from: Address, + _amount: TokenAmount, + _data: &[u8], + _bs: &Blockstore, + ) -> Result { + todo!() + } } diff --git a/testing/integration/Cargo.toml b/testing/integration/Cargo.toml new file mode 100644 index 00000000..eb3eea0f --- /dev/null +++ b/testing/integration/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "fvm_integration_tests" +version = "0.1.0" +repository = "https://github.com/helix-collective/filecoin" \ No newline at end of file diff --git a/testing/integration/actors/fil_token_actor/Cargo.toml b/testing/integration/actors/fil_token_actor/Cargo.toml new file mode 100644 index 00000000..cadb6cb7 --- /dev/null +++ b/testing/integration/actors/fil_token_actor/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "fil_token_actor" +version = "0.1.0" +repository = "https://github.com/helix-collective/filecoin" +edition = "2021" + +[dependencies] +fvm_sdk = { version = "1.0.0 "} +fvm_shared = { version = "0.8.0" } + +[build-dependencies] +wasm-builder = "3.0.1" diff --git a/testing/integration/actors/fil_token_actor/build.rs b/testing/integration/actors/fil_token_actor/build.rs new file mode 100644 index 00000000..0f2aa8a5 --- /dev/null +++ b/testing/integration/actors/fil_token_actor/build.rs @@ -0,0 +1,12 @@ +fn main() { + use wasm_builder::WasmBuilder; + WasmBuilder::new() + .with_current_project() + .import_memory() + .append_to_rust_flags("-Ctarget-feature=+crt-static") + .append_to_rust_flags("-Cpanic=abort") + .append_to_rust_flags("-Coverflow-checks=true") + .append_to_rust_flags("-Clto=true") + .append_to_rust_flags("-Copt-level=z") + .build() +} diff --git a/testing/integration/actors/fil_token_actor/src/lib.rs b/testing/integration/actors/fil_token_actor/src/lib.rs new file mode 100644 index 00000000..77c0769b --- /dev/null +++ b/testing/integration/actors/fil_token_actor/src/lib.rs @@ -0,0 +1,11 @@ +use fvm_sdk as sdk; + +/// Placeholder invoke for testing +#[no_mangle] +pub fn invoke(_: u32) -> u32 { + // Conduct method dispatch. Handle input parameters and return data. + sdk::vm::abort( + fvm_shared::error::ExitCode::FIRST_USER_EXIT_CODE, + Some("sample abort"), + ) +} diff --git a/testing/integration/src/lib.rs b/testing/integration/src/lib.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/testing/integration/src/lib.rs @@ -0,0 +1 @@ + From 56df5582726ece27555ae98308787c0aac7c4499 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Wed, 6 Jul 2022 11:43:36 +1000 Subject: [PATCH 04/21] author an actor that uses fil_token --- fil_token/Cargo.toml | 2 -- rust-toolchain | 1 + .../actors/fil_token_actor/Cargo.toml | 1 + .../actors/fil_token_actor/src/lib.rs | 22 ++++++++++++++----- 4 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 rust-toolchain diff --git a/fil_token/Cargo.toml b/fil_token/Cargo.toml index 37997649..a6993e7e 100644 --- a/fil_token/Cargo.toml +++ b/fil_token/Cargo.toml @@ -3,8 +3,6 @@ name = "fil_token" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] anyhow = "1.0.56" cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 00000000..07ade694 --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +nightly \ No newline at end of file diff --git a/testing/integration/actors/fil_token_actor/Cargo.toml b/testing/integration/actors/fil_token_actor/Cargo.toml index cadb6cb7..0ed13891 100644 --- a/testing/integration/actors/fil_token_actor/Cargo.toml +++ b/testing/integration/actors/fil_token_actor/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] fvm_sdk = { version = "1.0.0 "} fvm_shared = { version = "0.8.0" } +fil_token = { version = "0.1.0", path = "../../../../fil_token" } [build-dependencies] wasm-builder = "3.0.1" diff --git a/testing/integration/actors/fil_token_actor/src/lib.rs b/testing/integration/actors/fil_token_actor/src/lib.rs index 77c0769b..fbfff123 100644 --- a/testing/integration/actors/fil_token_actor/src/lib.rs +++ b/testing/integration/actors/fil_token_actor/src/lib.rs @@ -1,11 +1,23 @@ +use fil_token; use fvm_sdk as sdk; /// Placeholder invoke for testing #[no_mangle] -pub fn invoke(_: u32) -> u32 { +pub fn invoke(params: u32) -> u32 { // Conduct method dispatch. Handle input parameters and return data. - sdk::vm::abort( - fvm_shared::error::ExitCode::FIRST_USER_EXIT_CODE, - Some("sample abort"), - ) + let method_num = sdk::message::method_number(); + + match method_num { + 1 => constructor(), + _ => { + sdk::vm::abort( + fvm_shared::error::ExitCode::FIRST_USER_EXIT_CODE, + Some("sample abort"), + ); + } + } +} + +fn constructor() -> u32 { + 0_u32 } From 788f80d0de953516c9107bc2d92dd38993a0f11e Mon Sep 17 00:00:00 2001 From: Alex Su Date: Thu, 7 Jul 2022 18:25:44 +1000 Subject: [PATCH 05/21] better params and return types --- .gitignore | 2 + fil_token/Cargo.toml | 3 +- fil_token/src/blockstore.rs | 1 + fil_token/src/lib.rs | 1 + fil_token/src/runtime/mod.rs | 1 + fil_token/src/token/errors.rs | 22 + fil_token/src/token/mod.rs | 632 ++++++------------ fil_token/src/token/state.rs | 296 ++++++++ fil_token/src/token/types.rs | 109 +++ .../actors/fil_token_actor/src/lib.rs | 3 +- 10 files changed, 653 insertions(+), 417 deletions(-) create mode 100644 fil_token/src/runtime/mod.rs create mode 100644 fil_token/src/token/errors.rs create mode 100644 fil_token/src/token/state.rs create mode 100644 fil_token/src/token/types.rs diff --git a/.gitignore b/.gitignore index 767dae23..6a4c1d6c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +.vscode \ No newline at end of file diff --git a/fil_token/Cargo.toml b/fil_token/Cargo.toml index a6993e7e..3e3f7575 100644 --- a/fil_token/Cargo.toml +++ b/fil_token/Cargo.toml @@ -13,4 +13,5 @@ fvm_ipld_amt = { version = "0.4.2", features = ["go-interop"] } fvm_ipld_encoding = "0.2.2" fvm_sdk = { version = "1.0.0" } fvm_shared = { version = "0.8.0" } -serde = { version = "1.0.136", features = ["derive"] } \ No newline at end of file +serde = { version = "1.0.136", features = ["derive"] } +serde_tuple = { version = "0.5.0" } \ No newline at end of file diff --git a/fil_token/src/blockstore.rs b/fil_token/src/blockstore.rs index 0de47843..2042bbea 100644 --- a/fil_token/src/blockstore.rs +++ b/fil_token/src/blockstore.rs @@ -26,6 +26,7 @@ impl fvm_ipld_blockstore::Blockstore for Blockstore { } Ok(()) } + fn put(&self, code: Code, block: &Block) -> Result where D: AsRef<[u8]>, diff --git a/fil_token/src/lib.rs b/fil_token/src/lib.rs index a6309255..3ff6b814 100644 --- a/fil_token/src/lib.rs +++ b/fil_token/src/lib.rs @@ -1,4 +1,5 @@ pub mod blockstore; +pub mod runtime; pub mod token; #[cfg(test)] diff --git a/fil_token/src/runtime/mod.rs b/fil_token/src/runtime/mod.rs new file mode 100644 index 00000000..8a7b7a68 --- /dev/null +++ b/fil_token/src/runtime/mod.rs @@ -0,0 +1 @@ +pub trait Runtime {} diff --git a/fil_token/src/token/errors.rs b/fil_token/src/token/errors.rs new file mode 100644 index 00000000..e04cf3f9 --- /dev/null +++ b/fil_token/src/token/errors.rs @@ -0,0 +1,22 @@ +use super::state::StateError; +use fvm_ipld_hamt::Error as HamtError; +use fvm_shared::address::Address; + +pub enum ActorError { + AddrNotFound(Address), + IpldState(StateError), + IpldHamt(HamtError), + Arithmetic(String), +} + +impl From for ActorError { + fn from(e: StateError) -> Self { + Self::IpldState(e) + } +} + +impl From for ActorError { + fn from(e: HamtError) -> Self { + Self::IpldHamt(e) + } +} diff --git a/fil_token/src/token/mod.rs b/fil_token/src/token/mod.rs index dc25f2db..11abcf53 100644 --- a/fil_token/src/token/mod.rs +++ b/fil_token/src/token/mod.rs @@ -1,74 +1,20 @@ +pub mod errors; pub mod receiver; +pub mod state; +pub mod types; -use std::ops::Add; -use std::ops::Sub; - -use anyhow::anyhow; -use cid::Cid; - -use cid::multihash::Code; - -use fvm_ipld_blockstore::Blockstore as Store; -use fvm_ipld_encoding::tuple::*; -use fvm_ipld_encoding::Cbor; -use fvm_ipld_encoding::CborStore; -use fvm_ipld_encoding::DAG_CBOR; -use fvm_ipld_hamt::Hamt; - +use fvm_ipld_blockstore::Blockstore as IpldStore; use fvm_shared::address::Address; -use fvm_shared::bigint::bigint_ser; use fvm_shared::bigint::bigint_ser::BigIntDe; use fvm_shared::bigint::BigInt; use fvm_shared::bigint::Zero; -pub use fvm_shared::econ::TokenAmount; +use fvm_shared::econ::TokenAmount; use fvm_shared::ActorID; -use fvm_shared::HAMT_BIT_WIDTH; - -use fvm_sdk::ipld; -use fvm_sdk::sself; - -use crate::blockstore::Blockstore; - -pub type Result = std::result::Result; - -type TransferResult = std::result::Result; -pub struct TokenAmountDiff { - pub required: TokenAmount, - pub actual: TokenAmount, -} - -pub enum TransferError { - NoRecrHook, - InsufficientAllowance(TokenAmountDiff), - InsufficientBalance(TokenAmountDiff), -} - -pub enum TokenError { - AddrNotFound(Address), - Arithmetic, - Ipld(fvm_ipld_hamt::Error), - Err(anyhow::Error), - Transfer(TransferError), -} - -impl From for TokenError { - fn from(e: anyhow::Error) -> Self { - Self::Err(e) - } -} - -impl From for TokenError { - fn from(e: fvm_ipld_hamt::Error) -> Self { - Self::Ipld(e) - } -} - -impl From for TokenError { - fn from(e: TransferError) -> Self { - Self::Transfer(e) - } -} +use self::errors::ActorError; +use self::state::TokenState; +use self::types::*; +use crate::runtime::Runtime; /// A macro to abort concisely. macro_rules! abort { @@ -80,333 +26,173 @@ macro_rules! abort { }; } +type Result = std::result::Result; + /// A standard fungible token interface allowing for on-chain transactions pub trait Token { + /// Constructs the token + fn constructor(&self, params: ConstructorParams) -> Result<()>; + + /// Returns the name of the token fn name(&self) -> String; + /// Returns the ticker symbol of the token fn symbol(&self) -> String; + /// Returns the total amount of the token in existence fn total_supply(&self) -> TokenAmount; /// Mint a number of tokens and assign them to a specific Actor - /// - /// Minting can only be done in the constructor as once off - /// TODO: allow authorised actors to mint more supply - fn mint( - &self, - amount: TokenAmount, - initial_holder: Address, - bs: &Blockstore, - ) -> Result; + fn mint(&self, params: MintParams) -> Result; /// Gets the balance of a particular address (if it exists). - fn balance_of(&self, holder: Address, bs: &Blockstore) -> Result; + fn balance_of(&self, params: Address) -> Result; /// Atomically increase the amount that a spender can pull from an account - fn increase_allowance( - &self, - spender: Address, - value: TokenAmount, - bs: &Blockstore, - ) -> Result; + /// + /// Returns the new allowance between those two addresses + fn increase_allowance(&self, params: ChangeAllowanceParams) -> Result; /// Atomically decrease the amount that a spender can pull from an account /// /// The allowance cannot go below 0 and will be capped if the requested decrease /// is more than the current allowance - fn decrease_allowance( - &self, - spender: Address, - value: TokenAmount, - bs: &Blockstore, - ) -> Result; - - fn revoke_allowance(&self, spender: Address, bs: &Blockstore) -> Result<()>; - - fn allowance(&self, owner: Address, spender: Address, bs: &Blockstore) -> Result; - - fn burn(&self, amount: TokenAmount, data: &[u8], bs: &Blockstore) -> Result; - - fn transfer_from( - &self, - owner: Address, - spender: Address, - amount: TokenAmount, - bs: &Blockstore, - ) -> Result; - - fn burn_from( - &self, - from: Address, - amount: TokenAmount, - data: &[u8], - bs: &Blockstore, - ) -> Result; -} - -/// Token state ipld structure -#[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] -pub struct DefaultToken { - #[serde(with = "bigint_ser")] - supply: TokenAmount, - name: String, - symbol: String, - - balances: Cid, - allowances: Cid, -} - -/// Default token implementation -impl DefaultToken { - pub fn new(name: &str, symbol: &str, store: &BS) -> anyhow::Result - where - BS: Store, - { - let empty_balance_map = Hamt::<_, ()>::new_with_bit_width(store, HAMT_BIT_WIDTH) - .flush() - .map_err(|e| anyhow!("Failed to create empty balances map state {}", e))?; - let empty_allowances_map = Hamt::<_, ()>::new_with_bit_width(store, HAMT_BIT_WIDTH) - .flush() - .map_err(|e| anyhow!("Failed to create empty balances map state {}", e))?; - - Ok(Self { - supply: Default::default(), - name: name.to_string(), - symbol: symbol.to_string(), - balances: empty_balance_map, - allowances: empty_allowances_map, - }) - } - - pub fn load() -> Self { - // First, load the current state root. - let root = match sself::root() { - Ok(root) => root, - Err(err) => abort!(USR_ILLEGAL_STATE, "failed to get root: {:?}", err), - }; - - // Load the actor state from the state tree. - match Blockstore.get_cbor::(&root) { - Ok(Some(state)) => state, - Ok(None) => abort!(USR_ILLEGAL_STATE, "state does not exist"), - Err(err) => abort!(USR_ILLEGAL_STATE, "failed to get state: {}", err), - } - } - - pub fn save(&self) -> Cid { - let serialized = match fvm_ipld_encoding::to_vec(self) { - Ok(s) => s, - Err(err) => abort!(USR_SERIALIZATION, "failed to serialize state: {:?}", err), - }; - let cid = match ipld::put(Code::Blake2b256.into(), 32, DAG_CBOR, serialized.as_slice()) { - Ok(cid) => cid, - Err(err) => abort!(USR_SERIALIZATION, "failed to store initial state: {:}", err), - }; - if let Err(err) = sself::set_root(&cid) { - abort!(USR_ILLEGAL_STATE, "failed to set root ciid: {:}", err); - } - cid - } + fn decrease_allowance(&self, params: ChangeAllowanceParams) -> Result; - fn get_balance_map(&self, bs: &Blockstore) -> Hamt { - let balances = match Hamt::::load(&self.balances, *bs) { - Ok(map) => map, - Err(err) => abort!(USR_ILLEGAL_STATE, "Failed to load balances hamt: {:?}", err), - }; - balances - } + /// Revoke the allowance between two addresses by setting it to 0 + fn revoke_allowance(&self, params: RevokeAllowanceParams) -> Result; - /// Get the global allowances map + /// Get the allowance between two addresses /// - /// Gets a HAMT with CIDs linking to other HAMTs - fn get_allowances_map(&self, bs: &Blockstore) -> Hamt { - let allowances = match Hamt::::load(&self.allowances, *bs) { - Ok(map) => map, - Err(err) => abort!( - USR_ILLEGAL_STATE, - "Failed to load allowances hamt: {:?}", - err - ), - }; - allowances - } - - /// Get the allowances map of a specific actor, lazily creating one if it didn't exist - fn get_actor_allowance_map( - &self, - bs: &Blockstore, - authoriser: ActorID, - ) -> Hamt { - let mut global_allowances = self.get_allowances_map(bs); - match global_allowances.get(&authoriser) { - Ok(Some(map)) => { - // authorising actor already had an allowance map, return it - Hamt::::load(map, *bs).unwrap() - } - Ok(None) => { - // authorising actor does not have an allowance map, create one and return it - let mut new_actor_allowances = Hamt::new(*bs); - let cid = new_actor_allowances - .flush() - .map_err(|e| anyhow!("Failed to create empty balances map state {}", e)) - .unwrap(); - global_allowances.set(authoriser, cid).unwrap(); - new_actor_allowances - } - Err(e) => abort!( - USR_ILLEGAL_STATE, - "failed to get actor's allowance map {:?}", - e - ), - } - } - - fn enough_allowance( - &self, - from: ActorID, - spender: ActorID, - to: ActorID, - amount: &TokenAmount, - bs: &Blockstore, - ) -> std::result::Result<(), TokenAmountDiff> { - if spender == from { - return std::result::Result::Ok(()); - } - - let allowances = self.get_actor_allowance_map(bs, from); - let allowance = match allowances.get(&to) { - Ok(Some(amount)) => amount.0.clone(), - _ => TokenAmount::zero(), - }; + /// The spender can burn or transfer the allowance amount out of the owner's address + fn allowance(&self, params: GetAllowanceParams) -> Result; - if allowance.lt(&amount) { - Err(TokenAmountDiff { - actual: allowance, - required: amount.clone(), - }) - } else { - std::result::Result::Ok(()) - } - } - - fn enough_balance( - &self, - from: ActorID, - amount: &TokenAmount, - bs: &Blockstore, - ) -> std::result::Result<(), TokenAmountDiff> { - let balances = self.get_balance_map(bs); - let balance = match balances.get(&from) { - Ok(Some(amount)) => amount.0.clone(), - _ => TokenAmount::zero(), - }; - - if balance.lt(&amount) { - Err(TokenAmountDiff { - actual: balance, - required: amount.clone(), - }) - } else { - std::result::Result::Ok(()) - } - } - - /// Atomically make a transfer - fn make_transfer( - &self, - bs: &Blockstore, - amount: &TokenAmount, - from: ActorID, - spender: ActorID, - to: ActorID, - ) -> TransferResult { - if let Err(e) = self.enough_allowance(from, spender, to, amount, bs) { - return Err(TransferError::InsufficientAllowance(e)); - } - if let Err(e) = self.enough_balance(from, amount, bs) { - return Err(TransferError::InsufficientBalance(e)); - } - - // Decrease allowance, decrease balance - // From the above checks, we know these exist - // TODO: do this in a transaction to avoid re-entrancy bugs - let mut allowances = self.get_actor_allowance_map(bs, from); - let allowance = allowances.get(&to).unwrap().unwrap(); - let new_allowance = allowance.0.clone().sub(amount); - allowances.set(to, BigIntDe(new_allowance)).unwrap(); - - let mut balances = self.get_balance_map(bs); - let sender_balance = balances.get(&from).unwrap().unwrap(); - let new_sender_balance = sender_balance.0.clone().sub(amount); - balances.set(from, BigIntDe(new_sender_balance)).unwrap(); + /// Burn tokens from a specified account, decreasing the total supply + fn burn(&self, params: BurnParams) -> Result; - // TODO: call the receive hook - - // TODO: if no hook, revert the balance and allowance change - - // if successful, mark the balance as having been credited - - let receiver_balance = balances.get(&to).unwrap().unwrap(); - let new_receiver_balance = receiver_balance.0.clone().add(amount); - balances.set(to, BigIntDe(new_receiver_balance)).unwrap(); + /// Transfer between two addresses + fn transfer_from(&self, params: TransferParams) -> Result; +} - Ok(amount.clone()) - } +/// Holds injectable services to access/interface with IPLD/FVM layer +pub struct StandardToken +where + BS: IpldStore + Copy, + FVM: Runtime, +{ + /// Injected blockstore + bs: BS, + /// Access to the runtime + fvm: FVM, } +/// Resolve an address to an ActorID fn resolve_address(address: &Address) -> Result { match fvm_sdk::actor::resolve_address(address) { Some(addr) => Ok(addr), - None => Err(TokenError::AddrNotFound(*address)), + None => Err(ActorError::AddrNotFound(*address)), + } +} + +impl StandardToken +where + BS: IpldStore + Copy, + FVM: Runtime, +{ + fn load_state(&self) -> TokenState { + TokenState::load(&self.bs) } } -impl Cbor for DefaultToken {} +impl Token for StandardToken +where + BS: IpldStore + Copy, + FVM: Runtime, +{ + fn constructor(&self, params: ConstructorParams) -> Result<()> { + let init_state = TokenState::new(&self.bs, ¶ms.name, ¶ms.symbol)?; + init_state.save(&self.bs); + + let mint_params = params.mint_params; + self.mint(mint_params); + Ok(()) + } -impl Token for DefaultToken { fn name(&self) -> String { - let state = Self::load(); + let state = self.load_state(); state.name } fn symbol(&self) -> String { - let state = Self::load(); + let state = self.load_state(); state.symbol } fn total_supply(&self) -> TokenAmount { - let state = Self::load(); + let state = self.load_state(); state.supply } - fn mint(&self, amount: TokenAmount, treasury: Address, bs: &Blockstore) -> Result { + fn mint(&self, params: MintParams) -> Result { // TODO: check we are being called in the constructor by init system actor + // - or that other (TBD) minting rules are satified - let mut state = Self::load(); - let mut balances = self.get_balance_map(bs); + // these should be injectable by the token author + let mut state = self.load_state(); + let mut balances = state.get_balance_map(&self.bs); - let treasury = match fvm_sdk::actor::resolve_address(&treasury) { + // FIXME: replace fvm_sdk with abstraction + let holder = match fvm_sdk::actor::resolve_address(¶ms.initial_holder) { Some(id) => id, - None => return Err(TokenError::AddrNotFound(treasury)), + None => { + return Ok(MintReturn { + newly_minted: TokenAmount::zero(), + successful: false, + total_supply: state.supply, + }) + } }; // Mint the tokens into a specified account - balances.set(treasury, BigIntDe(amount.clone()))?; + let balance = balances + .delete(&holder)? + .map(|de| de.1 .0) + .unwrap_or(TokenAmount::zero()); + let new_balance = balance + .checked_add(¶ms.value) + .ok_or(ActorError::Arithmetic(String::from( + "Minting into caused overflow", + )))?; + balances.set(holder, BigIntDe(new_balance))?; // set the global supply of the contract - state.supply = amount.clone(); - - Ok(amount) + let new_supply = state + .supply + .checked_add(¶ms.value) + .ok_or(ActorError::Arithmetic(String::from( + "Minting caused total supply to overflow", + )))?; + state.supply = new_supply; + + // commit the state if supply and balance increased + state.save(&self.bs); + + Ok(MintReturn { + newly_minted: params.value, + successful: true, + total_supply: state.supply, + }) } - fn balance_of(&self, holder: Address, bs: &Blockstore) -> Result { + fn balance_of(&self, holder: Address) -> Result { // Load the HAMT holding balances - let balances = self.get_balance_map(bs); + let state = self.load_state(); + let balances = state.get_balance_map(&self.bs); // Resolve the address let addr_id = match fvm_sdk::actor::resolve_address(&holder) { Some(id) => id, - None => return Err(TokenError::AddrNotFound(holder)), + None => return Err(ActorError::AddrNotFound(holder)), }; match balances.get(&addr_id) { @@ -420,123 +206,141 @@ impl Token for DefaultToken { } } - fn increase_allowance( - &self, - spender: Address, - value: TokenAmount, - bs: &Blockstore, - ) -> Result { - let caller_id = fvm_sdk::message::caller(); + fn increase_allowance(&self, params: ChangeAllowanceParams) -> Result { + // Load the HAMT holding balances + let state = self.load_state(); - let caller_allowances_map = self.get_actor_allowance_map(bs, caller_id); + // FIXME: replace with runtime service call + let caller_id = fvm_sdk::message::caller(); + let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id); - let spender = match fvm_sdk::actor::resolve_address(&spender) { + let spender = match fvm_sdk::actor::resolve_address(¶ms.spender) { Some(id) => id, - None => return Err(TokenError::AddrNotFound(spender)), + None => return Err(ActorError::AddrNotFound(params.spender)), }; let new_amount = match caller_allowances_map.get(&spender)? { - Some(existing_allowance) => existing_allowance.0.checked_add(&value), - None => todo!(), + // Allowance exists - attempt to calculate new allowance + Some(existing_allowance) => match existing_allowance.0.checked_add(¶ms.value) { + Some(new_allowance) => { + caller_allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; + new_allowance + } + None => return Err(ActorError::Arithmetic(String::from("Allowance overflowed"))), + }, + // No allowance recorded previously + None => { + caller_allowances_map.set(spender, BigIntDe(params.value.clone())); + params.value + } }; - match new_amount { - Some(amount) => Ok(amount), - None => Err(TokenError::Arithmetic), - } - } + state.save(&self.bs); - fn decrease_allowance( - &self, - spender: Address, - value: TokenAmount, - bs: &Blockstore, - ) -> Result { - let caller_id = fvm_sdk::message::caller(); + Ok(AllowanceReturn { + owner: params.owner, + spender: params.spender, + value: new_amount, + }) + } - // TODO: can exit earlier if the authorisers map doesn't even exist - let mut caller_allowances_map = self.get_actor_allowance_map(bs, caller_id); + fn decrease_allowance(&self, params: ChangeAllowanceParams) -> Result { + // Load the HAMT holding balances + let state = self.load_state(); - let spender = match fvm_sdk::actor::resolve_address(&spender) { + // FIXME: replace with runtime service call + let caller_id = fvm_sdk::message::caller(); + let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id); + let spender = match fvm_sdk::actor::resolve_address(¶ms.spender) { Some(id) => id, - None => return Err(TokenError::AddrNotFound(spender)), + None => return Err(ActorError::AddrNotFound(params.spender)), }; - // check that the allowance is larger than the decrease - let existing_balance = match caller_allowances_map.get(&spender)? { + let new_allowance = match caller_allowances_map.get(&spender)? { Some(existing_allowance) => { - if existing_allowance.0.gt(&value) { - Some(existing_allowance.clone().0) - } else { - None - } + let new_allowance = existing_allowance + .0 + .checked_sub(¶ms.value) + .unwrap() // Unwrap should be safe as allowance always > 0 + .max(BigInt::zero()); + caller_allowances_map.set(spender, BigIntDe(new_allowance.clone())); + new_allowance + } + None => { + // Can't decrease non-existent allowance + return Ok(AllowanceReturn { + owner: params.owner, + spender: params.spender, + value: TokenAmount::zero(), + }); } - None => None, }; - match existing_balance { - Some(existing_balance) => { - let new_allowance = existing_balance.sub(value); - caller_allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; - Ok(new_allowance) - } - _ => { - caller_allowances_map.set(spender, BigIntDe(BigInt::zero()))?; - Ok(BigInt::zero()) - } - } + state.save(&self.bs); + + Ok(AllowanceReturn { + owner: params.owner, + spender: params.spender, + value: new_allowance, + }) } - fn revoke_allowance(&self, _spender: Address, _bs: &Blockstore) -> Result<()> { - todo!() + fn revoke_allowance(&self, params: RevokeAllowanceParams) -> Result { + // Load the HAMT holding balances + let state = self.load_state(); + + // FIXME: replace with runtime service call + let caller_id = fvm_sdk::message::caller(); + let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id); + let spender = match fvm_sdk::actor::resolve_address(¶ms.spender) { + Some(id) => id, + None => return Err(ActorError::AddrNotFound(params.spender)), + }; + + let new_allowance = TokenAmount::zero(); + caller_allowances_map.set(spender, BigIntDe(new_allowance.clone())); + state.save(&self.bs); + + Ok(AllowanceReturn { + owner: params.owner, + spender: params.spender, + value: new_allowance, + }) } - fn allowance(&self, owner: Address, spender: Address, bs: &Blockstore) -> Result { - let owner = match fvm_sdk::actor::resolve_address(&owner) { + fn allowance(&self, params: GetAllowanceParams) -> Result { + // Load the HAMT holding balances + let state = self.load_state(); + + // FIXME: replace with runtime service call + let owner = match fvm_sdk::actor::resolve_address(¶ms.owner) { Some(id) => id, - None => return Err(TokenError::AddrNotFound(owner)), + None => return Err(ActorError::AddrNotFound(params.spender)), }; - let spender = match fvm_sdk::actor::resolve_address(&spender) { + + let owner_allowances_map = state.get_actor_allowance_map(&self.bs, owner); + let spender = match fvm_sdk::actor::resolve_address(¶ms.spender) { Some(id) => id, - None => return Err(TokenError::AddrNotFound(spender)), + None => return Err(ActorError::AddrNotFound(params.spender)), }; - let allowance_map = self.get_actor_allowance_map(bs, owner); - match allowance_map.get(&spender)? { - Some(allowance) => Ok(allowance.0.clone()), - None => Ok(TokenAmount::zero()), - } - } + let allowance = match owner_allowances_map.get(&spender)? { + Some(allowance) => allowance.0.clone(), + None => TokenAmount::zero(), + }; - fn burn(&self, _amount: TokenAmount, _data: &[u8], _bs: &Blockstore) -> Result { - todo!() + Ok(AllowanceReturn { + owner: params.owner, + spender: params.spender, + value: allowance, + }) } - fn transfer_from( - &self, - owner: Address, - receiver: Address, - amount: TokenAmount, - bs: &Blockstore, - ) -> Result { - let spender = fvm_sdk::message::caller(); - let owner = resolve_address(&owner)?; - let receiver = resolve_address(&receiver)?; - - let res = self.make_transfer(bs, &amount, owner, spender, receiver); - match res { - Ok(amount) => Ok(amount), - Err(e) => Err(e.into()), - } + fn burn(&self, params: BurnParams) -> Result { + todo!() } - fn burn_from( - &self, - _from: Address, - _amount: TokenAmount, - _data: &[u8], - _bs: &Blockstore, - ) -> Result { + fn transfer_from(&self, params: TransferParams) -> Result { todo!() } } diff --git a/fil_token/src/token/state.rs b/fil_token/src/token/state.rs new file mode 100644 index 00000000..a549fb92 --- /dev/null +++ b/fil_token/src/token/state.rs @@ -0,0 +1,296 @@ +use std::ops::Add; +use std::ops::Sub; + +use anyhow::anyhow; +use cid::multihash::Code; +use cid::Cid; + +use fvm_ipld_blockstore::Block; +use fvm_ipld_blockstore::Blockstore as IpldStore; +use fvm_ipld_encoding::tuple::*; +use fvm_ipld_encoding::Cbor; +use fvm_ipld_encoding::CborStore; +use fvm_ipld_encoding::DAG_CBOR; +use fvm_ipld_hamt::Hamt; +use fvm_sdk::sself; +use fvm_shared::address::Address; +use fvm_shared::bigint::bigint_ser; +use fvm_shared::bigint::bigint_ser::BigIntDe; +use fvm_shared::bigint::Zero; +use fvm_shared::econ::TokenAmount; +use fvm_shared::ActorID; +use fvm_shared::HAMT_BIT_WIDTH; + +use crate::blockstore::Blockstore; + +/// A macro to abort concisely. +macro_rules! abort { + ($code:ident, $msg:literal $(, $ex:expr)*) => { + fvm_sdk::vm::abort( + fvm_shared::error::ExitCode::$code.value(), + Some(format!($msg, $($ex,)*).as_str()), + ) + }; +} + +/// Token state ipld structure +#[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] +pub struct TokenState { + /// Total supply of token + #[serde(with = "bigint_ser")] + pub supply: TokenAmount, + /// Token name + pub name: String, + /// Ticker symbol + pub symbol: String, + + /// Map of balances as a Hamt + pub balances: Cid, + /// Map> as a Hamt + pub allowances: Cid, +} + +/// Functions to get and modify token state to and from the IPLD layer +impl TokenState { + pub fn new(store: &BS, name: &str, symbol: &str) -> StateResult { + let empty_balance_map = Hamt::<_, ()>::new_with_bit_width(store, HAMT_BIT_WIDTH) + .flush() + .map_err(|e| anyhow!("Failed to create empty balances map state {}", e))?; + let empty_allowances_map = Hamt::<_, ()>::new_with_bit_width(store, HAMT_BIT_WIDTH) + .flush() + .map_err(|e| anyhow!("Failed to create empty balances map state {}", e))?; + + Ok(Self { + supply: Default::default(), + name: name.to_string(), + symbol: symbol.to_string(), + balances: empty_balance_map, + allowances: empty_allowances_map, + }) + } + + /// Loads a fresh copy of the state from a blockstore + pub fn load(bs: &BS) -> Self { + // First, load the current state root + // TODO: inject sself capabilities + let root = match sself::root() { + Ok(root) => root, + Err(err) => abort!(USR_ILLEGAL_STATE, "failed to get root: {:?}", err), + }; + + // Load the actor state from the state tree. + match bs.get_cbor::(&root) { + Ok(Some(state)) => state, + Ok(None) => abort!(USR_ILLEGAL_STATE, "state does not exist"), + Err(err) => abort!(USR_ILLEGAL_STATE, "failed to get state: {}", err), + } + } + + /// Saves the current state to the blockstore + pub fn save(&self, bs: &BS) -> Cid { + let serialized = match fvm_ipld_encoding::to_vec(self) { + Ok(s) => s, + Err(err) => abort!(USR_SERIALIZATION, "failed to serialize state: {:?}", err), + }; + let block = Block { + codec: DAG_CBOR, + data: serialized, + }; + let cid = match bs.put(Code::Blake2b256.into(), &block) { + Ok(cid) => cid, + Err(err) => abort!(USR_SERIALIZATION, "failed to store initial state: {:}", err), + }; + if let Err(err) = sself::set_root(&cid) { + abort!(USR_ILLEGAL_STATE, "failed to set root ciid: {:}", err); + } + cid + } + + pub fn get_balance_map(&self, bs: &BS) -> Hamt { + let balances = match Hamt::::load(&self.balances, *bs) { + Ok(map) => map, + Err(err) => abort!(USR_ILLEGAL_STATE, "Failed to load balances hamt: {:?}", err), + }; + balances + } + + /// Get the global allowances map + /// + /// Gets a HAMT with CIDs linking to other HAMTs + pub fn get_allowances_map(&self, bs: &BS) -> Hamt { + let allowances = match Hamt::::load(&self.allowances, *bs) { + Ok(map) => map, + Err(err) => abort!( + USR_ILLEGAL_STATE, + "Failed to load allowances hamt: {:?}", + err + ), + }; + allowances + } + + /// Get the allowances map of a specific actor, lazily creating one if it didn't exist + pub fn get_actor_allowance_map( + &self, + bs: &BS, + owner: ActorID, + ) -> Hamt { + let mut global_allowances = self.get_allowances_map(bs); + match global_allowances.get(&owner) { + Ok(Some(map)) => { + // authorising actor already had an allowance map, return it + Hamt::::load(map, *bs).unwrap() + } + Ok(None) => { + // authorising actor does not have an allowance map, create one and return it + let mut new_actor_allowances = Hamt::new(*bs); + let cid = new_actor_allowances + .flush() + .map_err(|e| anyhow!("Failed to create empty balances map state {}", e)) + .unwrap(); + global_allowances.set(owner, cid).unwrap(); + new_actor_allowances + } + Err(e) => abort!( + USR_ILLEGAL_STATE, + "failed to get actor's allowance map {:?}", + e + ), + } + } + + // fn enough_allowance( + // &self, + // bs: &Blockstore, + // from: ActorID, + // spender: ActorID, + // to: ActorID, + // amount: &TokenAmount, + // ) -> std::result::Result<(), TokenAmountDiff> { + // if spender == from { + // return std::result::Result::Ok(()); + // } + + // let allowances = self.get_actor_allowance_map(bs, from); + // let allowance = match allowances.get(&to) { + // Ok(Some(amount)) => amount.0.clone(), + // _ => TokenAmount::zero(), + // }; + + // if allowance.lt(&amount) { + // Err(TokenAmountDiff { + // actual: allowance, + // required: amount.clone(), + // }) + // } else { + // std::result::Result::Ok(()) + // } + // } + + // fn enough_balance( + // &self, + // bs: &Blockstore, + // from: ActorID, + // amount: &TokenAmount, + // ) -> std::result::Result<(), TokenAmountDiff> { + // let balances = self.get_balance_map(bs); + // let balance = match balances.get(&from) { + // Ok(Some(amount)) => amount.0.clone(), + // _ => TokenAmount::zero(), + // }; + + // if balance.lt(&amount) { + // Err(TokenAmountDiff { + // actual: balance, + // required: amount.clone(), + // }) + // } else { + // std::result::Result::Ok(()) + // } + // } + + // /// Atomically make a transfer + // fn make_transfer( + // &self, + // bs: &Blockstore, + // amount: &TokenAmount, + // from: ActorID, + // spender: ActorID, + // to: ActorID, + // ) -> TransferResult { + // if let Err(e) = self.enough_allowance(bs, from, spender, to, amount) { + // return Err(TransferError::InsufficientAllowance(e)); + // } + // if let Err(e) = self.enough_balance(bs, from, amount) { + // return Err(TransferError::InsufficientBalance(e)); + // } + + // // Decrease allowance, decrease balance + // // From the above checks, we know these exist + // // TODO: do this in a transaction to avoid re-entrancy bugs + // let mut allowances = self.get_actor_allowance_map(bs, from); + // let allowance = allowances.get(&to).unwrap().unwrap(); + // let new_allowance = allowance.0.clone().sub(amount); + // allowances.set(to, BigIntDe(new_allowance)).unwrap(); + + // let mut balances = self.get_balance_map(bs); + // let sender_balance = balances.get(&from).unwrap().unwrap(); + // let new_sender_balance = sender_balance.0.clone().sub(amount); + // balances.set(from, BigIntDe(new_sender_balance)).unwrap(); + + // // TODO: call the receive hook + + // // TODO: if no hook, revert the balance and allowance change + + // // if successful, mark the balance as having been credited + + // let receiver_balance = balances.get(&to).unwrap().unwrap(); + // let new_receiver_balance = receiver_balance.0.clone().add(amount); + // balances.set(to, BigIntDe(new_receiver_balance)).unwrap(); + + // Ok(amount.clone()) + // } +} + +impl Cbor for TokenState {} + +pub struct TokenAmountDiff { + pub required: TokenAmount, + pub actual: TokenAmount, +} + +type TransferResult = std::result::Result; + +pub enum TransferError { + NoRecrHook, + InsufficientAllowance(TokenAmountDiff), + InsufficientBalance(TokenAmountDiff), +} + +pub enum StateError { + AddrNotFound(Address), + Arithmetic, + Ipld(fvm_ipld_hamt::Error), + Err(anyhow::Error), + Transfer(TransferError), +} + +pub type StateResult = std::result::Result; + +impl From for StateError { + fn from(e: anyhow::Error) -> Self { + Self::Err(e) + } +} + +impl From for StateError { + fn from(e: fvm_ipld_hamt::Error) -> Self { + Self::Ipld(e) + } +} + +impl From for StateError { + fn from(e: TransferError) -> Self { + Self::Transfer(e) + } +} diff --git a/fil_token/src/token/types.rs b/fil_token/src/token/types.rs new file mode 100644 index 00000000..c695bc4c --- /dev/null +++ b/fil_token/src/token/types.rs @@ -0,0 +1,109 @@ +use fvm_ipld_encoding::tuple::*; +use fvm_ipld_encoding::{Cbor, RawBytes}; +use fvm_shared::address::Address; +use fvm_shared::bigint::bigint_ser; +use fvm_shared::econ::TokenAmount; + +#[derive(Serialize_tuple, Deserialize_tuple)] +pub struct ConstructorParams { + pub mint_params: MintParams, + pub name: String, + pub symbol: String, +} + +/// Called during construction of the token actor to set a supply +#[derive(Serialize_tuple, Deserialize_tuple)] +pub struct MintParams { + pub initial_holder: Address, + #[serde(with = "bigint_ser")] + pub value: TokenAmount, +} + +#[derive(Serialize_tuple, Deserialize_tuple)] +pub struct MintReturn { + pub successful: bool, + #[serde(with = "bigint_ser")] + pub newly_minted: TokenAmount, + #[serde(with = "bigint_ser")] + pub total_supply: TokenAmount, +} + +impl Cbor for MintParams {} +impl Cbor for MintReturn {} + +/// An amount to increase or decrease an allowance by +#[derive(Serialize_tuple, Deserialize_tuple)] +pub struct ChangeAllowanceParams { + pub owner: Address, + pub spender: Address, + #[serde(with = "bigint_ser")] + pub value: TokenAmount, +} + +/// Params to get allowance between to addresses +#[derive(Serialize_tuple, Deserialize_tuple)] +pub struct GetAllowanceParams { + pub owner: Address, + pub spender: Address, +} + +/// Instruction to revoke (set to 0) an allowance +#[derive(Serialize_tuple, Deserialize_tuple)] +pub struct RevokeAllowanceParams { + pub owner: Address, + pub spender: Address, +} + +/// The updated value after allowance is increased or decreased +#[derive(Serialize_tuple, Deserialize_tuple)] +pub struct AllowanceReturn { + pub owner: Address, + pub spender: Address, + #[serde(with = "bigint_ser")] + pub value: TokenAmount, +} + +impl Cbor for ChangeAllowanceParams {} +impl Cbor for GetAllowanceParams {} +impl Cbor for RevokeAllowanceParams {} +impl Cbor for AllowanceReturn {} + +/// Burns an amount of token from an address +#[derive(Serialize_tuple, Deserialize_tuple)] +pub struct BurnParams { + pub owner: Address, + #[serde(with = "bigint_ser")] + pub value: TokenAmount, + pub data: RawBytes, +} + +#[derive(Serialize_tuple, Deserialize_tuple)] +pub struct BurnReturn { + pub owner: Address, + #[serde(with = "bigint_ser")] + pub burnt: TokenAmount, + #[serde(with = "bigint_ser")] + pub remaining_balance: TokenAmount, +} + +impl Cbor for BurnParams {} +impl Cbor for BurnReturn {} + +#[derive(Serialize_tuple, Deserialize_tuple)] +pub struct TransferParams { + pub from: Address, + pub to: Address, + #[serde(with = "bigint_ser")] + pub value: TokenAmount, +} + +#[derive(Serialize_tuple, Deserialize_tuple)] +pub struct TransferReturn { + pub from: Address, + pub to: Address, + #[serde(with = "bigint_ser")] + pub value: TokenAmount, +} + +impl Cbor for TransferParams {} +impl Cbor for TransferReturn {} diff --git a/testing/integration/actors/fil_token_actor/src/lib.rs b/testing/integration/actors/fil_token_actor/src/lib.rs index fbfff123..079498eb 100644 --- a/testing/integration/actors/fil_token_actor/src/lib.rs +++ b/testing/integration/actors/fil_token_actor/src/lib.rs @@ -1,9 +1,8 @@ -use fil_token; use fvm_sdk as sdk; /// Placeholder invoke for testing #[no_mangle] -pub fn invoke(params: u32) -> u32 { +pub fn invoke(_params: u32) -> u32 { // Conduct method dispatch. Handle input parameters and return data. let method_num = sdk::message::method_number(); From 18759e24430dea196ffa763e5e7cb93fa5a7ef3e Mon Sep 17 00:00:00 2001 From: Alex Su Date: Thu, 7 Jul 2022 18:39:09 +1000 Subject: [PATCH 06/21] cleanup --- fil_token/src/token/mod.rs | 38 ++++++++++++------------------------ fil_token/src/token/state.rs | 20 +++++-------------- fvm_dispatch/src/hash.rs | 2 +- 3 files changed, 18 insertions(+), 42 deletions(-) diff --git a/fil_token/src/token/mod.rs b/fil_token/src/token/mod.rs index 11abcf53..7b98d268 100644 --- a/fil_token/src/token/mod.rs +++ b/fil_token/src/token/mod.rs @@ -9,7 +9,6 @@ use fvm_shared::bigint::bigint_ser::BigIntDe; use fvm_shared::bigint::BigInt; use fvm_shared::bigint::Zero; use fvm_shared::econ::TokenAmount; -use fvm_shared::ActorID; use self::errors::ActorError; use self::state::TokenState; @@ -83,15 +82,7 @@ where /// Injected blockstore bs: BS, /// Access to the runtime - fvm: FVM, -} - -/// Resolve an address to an ActorID -fn resolve_address(address: &Address) -> Result { - match fvm_sdk::actor::resolve_address(address) { - Some(addr) => Ok(addr), - None => Err(ActorError::AddrNotFound(*address)), - } + _fvm: FVM, } impl StandardToken @@ -114,7 +105,7 @@ where init_state.save(&self.bs); let mint_params = params.mint_params; - self.mint(mint_params); + self.mint(mint_params)?; Ok(()) } @@ -157,21 +148,16 @@ where let balance = balances .delete(&holder)? .map(|de| de.1 .0) - .unwrap_or(TokenAmount::zero()); + .unwrap_or_else(TokenAmount::zero); let new_balance = balance .checked_add(¶ms.value) - .ok_or(ActorError::Arithmetic(String::from( - "Minting into caused overflow", - )))?; + .ok_or_else(|| ActorError::Arithmetic(String::from("Minting into caused overflow")))?; balances.set(holder, BigIntDe(new_balance))?; // set the global supply of the contract - let new_supply = state - .supply - .checked_add(¶ms.value) - .ok_or(ActorError::Arithmetic(String::from( - "Minting caused total supply to overflow", - )))?; + let new_supply = state.supply.checked_add(¶ms.value).ok_or_else(|| { + ActorError::Arithmetic(String::from("Minting caused total supply to overflow")) + })?; state.supply = new_supply; // commit the state if supply and balance increased @@ -230,7 +216,7 @@ where }, // No allowance recorded previously None => { - caller_allowances_map.set(spender, BigIntDe(params.value.clone())); + caller_allowances_map.set(spender, BigIntDe(params.value.clone()))?; params.value } }; @@ -263,7 +249,7 @@ where .checked_sub(¶ms.value) .unwrap() // Unwrap should be safe as allowance always > 0 .max(BigInt::zero()); - caller_allowances_map.set(spender, BigIntDe(new_allowance.clone())); + caller_allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; new_allowance } None => { @@ -298,7 +284,7 @@ where }; let new_allowance = TokenAmount::zero(); - caller_allowances_map.set(spender, BigIntDe(new_allowance.clone())); + caller_allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; state.save(&self.bs); Ok(AllowanceReturn { @@ -336,11 +322,11 @@ where }) } - fn burn(&self, params: BurnParams) -> Result { + fn burn(&self, _params: BurnParams) -> Result { todo!() } - fn transfer_from(&self, params: TransferParams) -> Result { + fn transfer_from(&self, _params: TransferParams) -> Result { todo!() } } diff --git a/fil_token/src/token/state.rs b/fil_token/src/token/state.rs index a549fb92..af54b544 100644 --- a/fil_token/src/token/state.rs +++ b/fil_token/src/token/state.rs @@ -1,6 +1,3 @@ -use std::ops::Add; -use std::ops::Sub; - use anyhow::anyhow; use cid::multihash::Code; use cid::Cid; @@ -16,13 +13,10 @@ use fvm_sdk::sself; use fvm_shared::address::Address; use fvm_shared::bigint::bigint_ser; use fvm_shared::bigint::bigint_ser::BigIntDe; -use fvm_shared::bigint::Zero; use fvm_shared::econ::TokenAmount; use fvm_shared::ActorID; use fvm_shared::HAMT_BIT_WIDTH; -use crate::blockstore::Blockstore; - /// A macro to abort concisely. macro_rules! abort { ($code:ident, $msg:literal $(, $ex:expr)*) => { @@ -96,7 +90,7 @@ impl TokenState { codec: DAG_CBOR, data: serialized, }; - let cid = match bs.put(Code::Blake2b256.into(), &block) { + let cid = match bs.put(Code::Blake2b256, &block) { Ok(cid) => cid, Err(err) => abort!(USR_SERIALIZATION, "failed to store initial state: {:}", err), }; @@ -107,26 +101,24 @@ impl TokenState { } pub fn get_balance_map(&self, bs: &BS) -> Hamt { - let balances = match Hamt::::load(&self.balances, *bs) { + match Hamt::::load(&self.balances, *bs) { Ok(map) => map, Err(err) => abort!(USR_ILLEGAL_STATE, "Failed to load balances hamt: {:?}", err), - }; - balances + } } /// Get the global allowances map /// /// Gets a HAMT with CIDs linking to other HAMTs pub fn get_allowances_map(&self, bs: &BS) -> Hamt { - let allowances = match Hamt::::load(&self.allowances, *bs) { + match Hamt::::load(&self.allowances, *bs) { Ok(map) => map, Err(err) => abort!( USR_ILLEGAL_STATE, "Failed to load allowances hamt: {:?}", err ), - }; - allowances + } } /// Get the allowances map of a specific actor, lazily creating one if it didn't exist @@ -259,8 +251,6 @@ pub struct TokenAmountDiff { pub actual: TokenAmount, } -type TransferResult = std::result::Result; - pub enum TransferError { NoRecrHook, InsufficientAllowance(TokenAmountDiff), diff --git a/fvm_dispatch/src/hash.rs b/fvm_dispatch/src/hash.rs index 673cdaee..43e87d51 100644 --- a/fvm_dispatch/src/hash.rs +++ b/fvm_dispatch/src/hash.rs @@ -52,7 +52,7 @@ impl MethodHasher { fn as_u32(bytes: &[u8]) -> u32 { ((bytes[0] as u32) << (8 * 3)) + ((bytes[1] as u32) << (8 * 2)) + - ((bytes[2] as u32) << (8 * 1)) + + ((bytes[2] as u32) << 8) + (bytes[3] as u32) } From 207528578b16f06008b0cf2d88d2a19182c1b4f2 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Mon, 11 Jul 2022 15:35:34 +1000 Subject: [PATCH 07/21] cleaner error handling --- fil_token/src/runtime/mod.rs | 9 ++++- fil_token/src/token/errors.rs | 53 ++++++++++++++++++++++++- fil_token/src/token/mod.rs | 69 +++++++++++---------------------- fil_token/src/token/receiver.rs | 5 +-- fil_token/src/token/state.rs | 43 +------------------- 5 files changed, 85 insertions(+), 94 deletions(-) diff --git a/fil_token/src/runtime/mod.rs b/fil_token/src/runtime/mod.rs index 8a7b7a68..29b192da 100644 --- a/fil_token/src/runtime/mod.rs +++ b/fil_token/src/runtime/mod.rs @@ -1 +1,8 @@ -pub trait Runtime {} +use anyhow::Result; +use fvm_shared::address::Address; + +pub trait Runtime { + fn caller(&self) -> u64; + + fn resolve_address(&self, addr: &Address) -> Result; +} diff --git a/fil_token/src/token/errors.rs b/fil_token/src/token/errors.rs index e04cf3f9..19e45f94 100644 --- a/fil_token/src/token/errors.rs +++ b/fil_token/src/token/errors.rs @@ -1,14 +1,57 @@ -use super::state::StateError; +use std::{error::Error, fmt::Display}; + use fvm_ipld_hamt::Error as HamtError; use fvm_shared::address::Address; +#[derive(Debug)] +pub enum RuntimeError { + AddrNotFound(Address), +} + +impl Display for RuntimeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RuntimeError::AddrNotFound(_) => write!(f, "Address not found: {}", self), + } + } +} + +impl Error for RuntimeError {} + +#[derive(Debug)] +pub enum StateError {} + +impl Display for StateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "State error") + } +} + +impl Error for StateError {} + +#[derive(Debug)] pub enum ActorError { AddrNotFound(Address), + Arithmetic(String), IpldState(StateError), IpldHamt(HamtError), - Arithmetic(String), + RuntimeError(RuntimeError), } +impl Display for ActorError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ActorError::AddrNotFound(e) => write!(f, "{}", e), + ActorError::Arithmetic(e) => write!(f, "{}", e), + ActorError::IpldState(e) => write!(f, "{}", e), + ActorError::IpldHamt(e) => write!(f, "{}", e), + ActorError::RuntimeError(e) => write!(f, "{}", e), + } + } +} + +impl Error for ActorError {} + impl From for ActorError { fn from(e: StateError) -> Self { Self::IpldState(e) @@ -20,3 +63,9 @@ impl From for ActorError { Self::IpldHamt(e) } } + +impl From for ActorError { + fn from(e: RuntimeError) -> Self { + ActorError::RuntimeError(e) + } +} diff --git a/fil_token/src/token/mod.rs b/fil_token/src/token/mod.rs index 7b98d268..f0adedc5 100644 --- a/fil_token/src/token/mod.rs +++ b/fil_token/src/token/mod.rs @@ -2,7 +2,13 @@ pub mod errors; pub mod receiver; pub mod state; pub mod types; +use self::errors::ActorError; +use self::state::TokenState; +use self::types::*; +use crate::runtime::Runtime; +use anyhow::bail; +use anyhow::Result; use fvm_ipld_blockstore::Blockstore as IpldStore; use fvm_shared::address::Address; use fvm_shared::bigint::bigint_ser::BigIntDe; @@ -10,11 +16,6 @@ use fvm_shared::bigint::BigInt; use fvm_shared::bigint::Zero; use fvm_shared::econ::TokenAmount; -use self::errors::ActorError; -use self::state::TokenState; -use self::types::*; -use crate::runtime::Runtime; - /// A macro to abort concisely. macro_rules! abort { ($code:ident, $msg:literal $(, $ex:expr)*) => { @@ -25,8 +26,6 @@ macro_rules! abort { }; } -type Result = std::result::Result; - /// A standard fungible token interface allowing for on-chain transactions pub trait Token { /// Constructs the token @@ -82,7 +81,7 @@ where /// Injected blockstore bs: BS, /// Access to the runtime - _fvm: FVM, + fvm: FVM, } impl StandardToken @@ -132,10 +131,9 @@ where let mut state = self.load_state(); let mut balances = state.get_balance_map(&self.bs); - // FIXME: replace fvm_sdk with abstraction - let holder = match fvm_sdk::actor::resolve_address(¶ms.initial_holder) { - Some(id) => id, - None => { + let holder = match self.fvm.resolve_address(¶ms.initial_holder) { + Ok(id) => id, + Err(_) => { return Ok(MintReturn { newly_minted: TokenAmount::zero(), successful: false, @@ -176,10 +174,7 @@ where let balances = state.get_balance_map(&self.bs); // Resolve the address - let addr_id = match fvm_sdk::actor::resolve_address(&holder) { - Some(id) => id, - None => return Err(ActorError::AddrNotFound(holder)), - }; + let addr_id = self.fvm.resolve_address(&holder)?; match balances.get(&addr_id) { Ok(Some(bal)) => Ok(bal.clone().0), @@ -196,15 +191,10 @@ where // Load the HAMT holding balances let state = self.load_state(); - // FIXME: replace with runtime service call - let caller_id = fvm_sdk::message::caller(); + let caller_id = self.fvm.caller(); let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id); - let spender = match fvm_sdk::actor::resolve_address(¶ms.spender) { - Some(id) => id, - None => return Err(ActorError::AddrNotFound(params.spender)), - }; - + let spender = self.fvm.resolve_address(&¶ms.spender)?; let new_amount = match caller_allowances_map.get(&spender)? { // Allowance exists - attempt to calculate new allowance Some(existing_allowance) => match existing_allowance.0.checked_add(¶ms.value) { @@ -212,7 +202,7 @@ where caller_allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; new_allowance } - None => return Err(ActorError::Arithmetic(String::from("Allowance overflowed"))), + None => bail!(ActorError::Arithmetic(String::from("Allowance overflowed"))), }, // No allowance recorded previously None => { @@ -234,13 +224,10 @@ where // Load the HAMT holding balances let state = self.load_state(); - // FIXME: replace with runtime service call - let caller_id = fvm_sdk::message::caller(); + let caller_id = self.fvm.caller(); let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id); - let spender = match fvm_sdk::actor::resolve_address(¶ms.spender) { - Some(id) => id, - None => return Err(ActorError::AddrNotFound(params.spender)), - }; + + let spender = self.fvm.resolve_address(&¶ms.spender)?; let new_allowance = match caller_allowances_map.get(&spender)? { Some(existing_allowance) => { @@ -275,16 +262,13 @@ where // Load the HAMT holding balances let state = self.load_state(); - // FIXME: replace with runtime service call - let caller_id = fvm_sdk::message::caller(); + let caller_id = self.fvm.caller(); let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id); - let spender = match fvm_sdk::actor::resolve_address(¶ms.spender) { - Some(id) => id, - None => return Err(ActorError::AddrNotFound(params.spender)), - }; + let spender = self.fvm.resolve_address(&¶ms.spender)?; let new_allowance = TokenAmount::zero(); caller_allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; + state.save(&self.bs); Ok(AllowanceReturn { @@ -298,17 +282,10 @@ where // Load the HAMT holding balances let state = self.load_state(); - // FIXME: replace with runtime service call - let owner = match fvm_sdk::actor::resolve_address(¶ms.owner) { - Some(id) => id, - None => return Err(ActorError::AddrNotFound(params.spender)), - }; - + let owner = self.fvm.resolve_address(¶ms.owner)?; let owner_allowances_map = state.get_actor_allowance_map(&self.bs, owner); - let spender = match fvm_sdk::actor::resolve_address(¶ms.spender) { - Some(id) => id, - None => return Err(ActorError::AddrNotFound(params.spender)), - }; + + let spender = self.fvm.resolve_address(&¶ms.spender)?; let allowance = match owner_allowances_map.get(&spender)? { Some(allowance) => allowance.0.clone(), diff --git a/fil_token/src/token/receiver.rs b/fil_token/src/token/receiver.rs index cf27518b..3c1a8424 100644 --- a/fil_token/src/token/receiver.rs +++ b/fil_token/src/token/receiver.rs @@ -1,9 +1,6 @@ +use anyhow::Result; use fvm_shared::{address::Address, econ::TokenAmount}; -pub type Result = std::result::Result; - -pub enum ReceiverError {} - /// Standard interface for a contract that wishes to receive tokens pub trait TokenReceiver { fn token_received(from: Address, amount: TokenAmount, data: &[u8]) -> Result; diff --git a/fil_token/src/token/state.rs b/fil_token/src/token/state.rs index af54b544..47ccfac7 100644 --- a/fil_token/src/token/state.rs +++ b/fil_token/src/token/state.rs @@ -1,4 +1,5 @@ use anyhow::anyhow; +use anyhow::Result; use cid::multihash::Code; use cid::Cid; @@ -10,7 +11,6 @@ use fvm_ipld_encoding::CborStore; use fvm_ipld_encoding::DAG_CBOR; use fvm_ipld_hamt::Hamt; use fvm_sdk::sself; -use fvm_shared::address::Address; use fvm_shared::bigint::bigint_ser; use fvm_shared::bigint::bigint_ser::BigIntDe; use fvm_shared::econ::TokenAmount; @@ -46,7 +46,7 @@ pub struct TokenState { /// Functions to get and modify token state to and from the IPLD layer impl TokenState { - pub fn new(store: &BS, name: &str, symbol: &str) -> StateResult { + pub fn new(store: &BS, name: &str, symbol: &str) -> Result { let empty_balance_map = Hamt::<_, ()>::new_with_bit_width(store, HAMT_BIT_WIDTH) .flush() .map_err(|e| anyhow!("Failed to create empty balances map state {}", e))?; @@ -245,42 +245,3 @@ impl TokenState { } impl Cbor for TokenState {} - -pub struct TokenAmountDiff { - pub required: TokenAmount, - pub actual: TokenAmount, -} - -pub enum TransferError { - NoRecrHook, - InsufficientAllowance(TokenAmountDiff), - InsufficientBalance(TokenAmountDiff), -} - -pub enum StateError { - AddrNotFound(Address), - Arithmetic, - Ipld(fvm_ipld_hamt::Error), - Err(anyhow::Error), - Transfer(TransferError), -} - -pub type StateResult = std::result::Result; - -impl From for StateError { - fn from(e: anyhow::Error) -> Self { - Self::Err(e) - } -} - -impl From for StateError { - fn from(e: fvm_ipld_hamt::Error) -> Self { - Self::Ipld(e) - } -} - -impl From for StateError { - fn from(e: TransferError) -> Self { - Self::Transfer(e) - } -} From 2750b240f80ea65c87f4d7b07eec9e631275f9d1 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Mon, 11 Jul 2022 16:12:46 +1000 Subject: [PATCH 08/21] add readme for token example actor --- .../actors/fil_token_actor/README.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 testing/integration/actors/fil_token_actor/README.md diff --git a/testing/integration/actors/fil_token_actor/README.md b/testing/integration/actors/fil_token_actor/README.md new file mode 100644 index 00000000..27c2ff71 --- /dev/null +++ b/testing/integration/actors/fil_token_actor/README.md @@ -0,0 +1,35 @@ +# FIL Token Actor + +This creates a FIP-??? compliant fungible token that wraps the value of of +native FIL. + +Calling the `mint` (non-standard) method on this actor will increase the +caller's WFIL balance by the amount of FIL sent in the message. The WFIL can be +transferred between any FIP-??? compliant wallets (which have `token_received` +hook) by either direct transfer or delegated transfer. + +https://etherscan.io/tx/0x96a7155b44b77c173e7c534ae1ceca536ba2ce534012ff844cf8c1737bc54921 + +## Direct transfer flow + +Call `transfer(TransferParams)` to directly transfer tokens from the caller's +wallet to a receiving address. + +## Delegated transfer flow + +1. Call `increase_allowance(AllowanceParams)` to increase the "spenders" + allowance +2. The "spender" can then call this actor with + `transfer_from(TransferFromParams)` to + +## Transferring tokens to this actor + +Transferring WFIL to the address of this actor itself will result in the WFIL +being burnt and the corresponding FIL being credited back. This prevents the +case where tokens are unintentionally locked in to contracts that are unable to +receive them. This flow requires the actor to implement its own `token_received` +hook. + +However, also compliant with the token standard would be for this actor to omit +the `token_received` hook. In this case, transfers to the contract itself would +simply be rejected, which also prevents unintentional loss of tokens. From 9383f3a669dd50770bcf67efc5b51566fb296c3b Mon Sep 17 00:00:00 2001 From: Alex Su Date: Mon, 11 Jul 2022 16:22:30 +1000 Subject: [PATCH 09/21] remove mint from token interface --- fil_token/src/token/mod.rs | 67 ++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/fil_token/src/token/mod.rs b/fil_token/src/token/mod.rs index f0adedc5..67eebe0c 100644 --- a/fil_token/src/token/mod.rs +++ b/fil_token/src/token/mod.rs @@ -40,9 +40,6 @@ pub trait Token { /// Returns the total amount of the token in existence fn total_supply(&self) -> TokenAmount; - /// Mint a number of tokens and assign them to a specific Actor - fn mint(&self, params: MintParams) -> Result; - /// Gets the balance of a particular address (if it exists). fn balance_of(&self, params: Address) -> Result; @@ -73,7 +70,7 @@ pub trait Token { } /// Holds injectable services to access/interface with IPLD/FVM layer -pub struct StandardToken +pub struct TokenHelper where BS: IpldStore + Copy, FVM: Runtime, @@ -84,7 +81,7 @@ where fvm: FVM, } -impl StandardToken +impl TokenHelper where BS: IpldStore + Copy, FVM: Runtime, @@ -92,38 +89,9 @@ where fn load_state(&self) -> TokenState { TokenState::load(&self.bs) } -} - -impl Token for StandardToken -where - BS: IpldStore + Copy, - FVM: Runtime, -{ - fn constructor(&self, params: ConstructorParams) -> Result<()> { - let init_state = TokenState::new(&self.bs, ¶ms.name, ¶ms.symbol)?; - init_state.save(&self.bs); - - let mint_params = params.mint_params; - self.mint(mint_params)?; - Ok(()) - } - - fn name(&self) -> String { - let state = self.load_state(); - state.name - } - - fn symbol(&self) -> String { - let state = self.load_state(); - state.symbol - } - - fn total_supply(&self) -> TokenAmount { - let state = self.load_state(); - state.supply - } - fn mint(&self, params: MintParams) -> Result { + // Utility function for token-authors to mint supply + pub fn mint(&self, params: MintParams) -> Result { // TODO: check we are being called in the constructor by init system actor // - or that other (TBD) minting rules are satified @@ -167,6 +135,33 @@ where total_supply: state.supply, }) } +} + +impl Token for TokenHelper +where + BS: IpldStore + Copy, + FVM: Runtime, +{ + fn constructor(&self, params: ConstructorParams) -> Result<()> { + let init_state = TokenState::new(&self.bs, ¶ms.name, ¶ms.symbol)?; + init_state.save(&self.bs); + Ok(()) + } + + fn name(&self) -> String { + let state = self.load_state(); + state.name + } + + fn symbol(&self) -> String { + let state = self.load_state(); + state.symbol + } + + fn total_supply(&self) -> TokenAmount { + let state = self.load_state(); + state.supply + } fn balance_of(&self, holder: Address) -> Result { // Load the HAMT holding balances From 950d95d7ded6750e4b7e38db50e26146c889bf25 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Mon, 11 Jul 2022 22:47:53 +1000 Subject: [PATCH 10/21] integration test --- Cargo.toml | 2 +- fil_token/src/runtime/fvm.rs | 18 ++ fil_token/src/runtime/mod.rs | 3 + fil_token/src/token/mod.rs | 40 ++--- fil_token/src/token/types.rs | 3 +- .../actors/fil_token_actor/src/lib.rs | 22 --- .../Cargo.toml | 4 +- .../README.md | 0 .../build.rs | 0 .../actors/wfil_token_actor/src/lib.rs | 155 ++++++++++++++++++ 10 files changed, 198 insertions(+), 49 deletions(-) create mode 100644 fil_token/src/runtime/fvm.rs delete mode 100644 testing/integration/actors/fil_token_actor/src/lib.rs rename testing/integration/actors/{fil_token_actor => wfil_token_actor}/Cargo.toml (74%) rename testing/integration/actors/{fil_token_actor => wfil_token_actor}/README.md (100%) rename testing/integration/actors/{fil_token_actor => wfil_token_actor}/build.rs (100%) create mode 100644 testing/integration/actors/wfil_token_actor/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 07001f69..a4aee29a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,5 @@ members = [ "fvm_dispatch", "fil_token", "testing/integration", - "testing/integration/actors/fil_token_actor", + "testing/integration/actors/wfil_token_actor", ] \ No newline at end of file diff --git a/fil_token/src/runtime/fvm.rs b/fil_token/src/runtime/fvm.rs new file mode 100644 index 00000000..72483ca9 --- /dev/null +++ b/fil_token/src/runtime/fvm.rs @@ -0,0 +1,18 @@ +use super::Runtime; + +use anyhow::{anyhow, Result}; +use fvm_sdk as sdk; +use sdk::actor; +use sdk::message; + +pub struct FvmRuntime {} + +impl Runtime for FvmRuntime { + fn caller(&self) -> u64 { + message::caller() + } + + fn resolve_address(&self, addr: &fvm_shared::address::Address) -> Result { + actor::resolve_address(addr).ok_or_else(|| anyhow!("Failed to resolve address")) + } +} diff --git a/fil_token/src/runtime/mod.rs b/fil_token/src/runtime/mod.rs index 29b192da..88378d02 100644 --- a/fil_token/src/runtime/mod.rs +++ b/fil_token/src/runtime/mod.rs @@ -1,3 +1,6 @@ +mod fvm; +pub use fvm::*; + use anyhow::Result; use fvm_shared::address::Address; diff --git a/fil_token/src/token/mod.rs b/fil_token/src/token/mod.rs index 67eebe0c..c54127ce 100644 --- a/fil_token/src/token/mod.rs +++ b/fil_token/src/token/mod.rs @@ -28,9 +28,6 @@ macro_rules! abort { /// A standard fungible token interface allowing for on-chain transactions pub trait Token { - /// Constructs the token - fn constructor(&self, params: ConstructorParams) -> Result<()>; - /// Returns the name of the token fn name(&self) -> String; @@ -86,8 +83,15 @@ where BS: IpldStore + Copy, FVM: Runtime, { - fn load_state(&self) -> TokenState { - TokenState::load(&self.bs) + pub fn new(bs: BS, fvm: FVM) -> Self { + Self { bs, fvm } + } + + /// Constructs the token + pub fn init_state(&self, params: ConstructorParams) -> Result<()> { + let init_state = TokenState::new(&self.bs, ¶ms.name, ¶ms.symbol)?; + init_state.save(&self.bs); + Ok(()) } // Utility function for token-authors to mint supply @@ -99,26 +103,15 @@ where let mut state = self.load_state(); let mut balances = state.get_balance_map(&self.bs); - let holder = match self.fvm.resolve_address(¶ms.initial_holder) { - Ok(id) => id, - Err(_) => { - return Ok(MintReturn { - newly_minted: TokenAmount::zero(), - successful: false, - total_supply: state.supply, - }) - } - }; - // Mint the tokens into a specified account let balance = balances - .delete(&holder)? + .delete(¶ms.initial_holder)? .map(|de| de.1 .0) .unwrap_or_else(TokenAmount::zero); let new_balance = balance .checked_add(¶ms.value) .ok_or_else(|| ActorError::Arithmetic(String::from("Minting into caused overflow")))?; - balances.set(holder, BigIntDe(new_balance))?; + balances.set(params.initial_holder, BigIntDe(new_balance))?; // set the global supply of the contract let new_supply = state.supply.checked_add(¶ms.value).ok_or_else(|| { @@ -135,6 +128,11 @@ where total_supply: state.supply, }) } + + /// Helper function that loads the root of the state tree related to token-accounting + fn load_state(&self) -> TokenState { + TokenState::load(&self.bs) + } } impl Token for TokenHelper @@ -142,12 +140,6 @@ where BS: IpldStore + Copy, FVM: Runtime, { - fn constructor(&self, params: ConstructorParams) -> Result<()> { - let init_state = TokenState::new(&self.bs, ¶ms.name, ¶ms.symbol)?; - init_state.save(&self.bs); - Ok(()) - } - fn name(&self) -> String { let state = self.load_state(); state.name diff --git a/fil_token/src/token/types.rs b/fil_token/src/token/types.rs index c695bc4c..4e0bf61d 100644 --- a/fil_token/src/token/types.rs +++ b/fil_token/src/token/types.rs @@ -3,6 +3,7 @@ use fvm_ipld_encoding::{Cbor, RawBytes}; use fvm_shared::address::Address; use fvm_shared::bigint::bigint_ser; use fvm_shared::econ::TokenAmount; +use fvm_shared::ActorID; #[derive(Serialize_tuple, Deserialize_tuple)] pub struct ConstructorParams { @@ -14,7 +15,7 @@ pub struct ConstructorParams { /// Called during construction of the token actor to set a supply #[derive(Serialize_tuple, Deserialize_tuple)] pub struct MintParams { - pub initial_holder: Address, + pub initial_holder: ActorID, #[serde(with = "bigint_ser")] pub value: TokenAmount, } diff --git a/testing/integration/actors/fil_token_actor/src/lib.rs b/testing/integration/actors/fil_token_actor/src/lib.rs deleted file mode 100644 index 079498eb..00000000 --- a/testing/integration/actors/fil_token_actor/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -use fvm_sdk as sdk; - -/// Placeholder invoke for testing -#[no_mangle] -pub fn invoke(_params: u32) -> u32 { - // Conduct method dispatch. Handle input parameters and return data. - let method_num = sdk::message::method_number(); - - match method_num { - 1 => constructor(), - _ => { - sdk::vm::abort( - fvm_shared::error::ExitCode::FIRST_USER_EXIT_CODE, - Some("sample abort"), - ); - } - } -} - -fn constructor() -> u32 { - 0_u32 -} diff --git a/testing/integration/actors/fil_token_actor/Cargo.toml b/testing/integration/actors/wfil_token_actor/Cargo.toml similarity index 74% rename from testing/integration/actors/fil_token_actor/Cargo.toml rename to testing/integration/actors/wfil_token_actor/Cargo.toml index 0ed13891..e4f5a34d 100644 --- a/testing/integration/actors/fil_token_actor/Cargo.toml +++ b/testing/integration/actors/wfil_token_actor/Cargo.toml @@ -1,10 +1,12 @@ [package] -name = "fil_token_actor" +name = "wfil_token_actor" version = "0.1.0" repository = "https://github.com/helix-collective/filecoin" edition = "2021" [dependencies] +anyhow = { version = "1.0.56" } +fvm_ipld_encoding = { version = "0.2.2" } fvm_sdk = { version = "1.0.0 "} fvm_shared = { version = "0.8.0" } fil_token = { version = "0.1.0", path = "../../../../fil_token" } diff --git a/testing/integration/actors/fil_token_actor/README.md b/testing/integration/actors/wfil_token_actor/README.md similarity index 100% rename from testing/integration/actors/fil_token_actor/README.md rename to testing/integration/actors/wfil_token_actor/README.md diff --git a/testing/integration/actors/fil_token_actor/build.rs b/testing/integration/actors/wfil_token_actor/build.rs similarity index 100% rename from testing/integration/actors/fil_token_actor/build.rs rename to testing/integration/actors/wfil_token_actor/build.rs diff --git a/testing/integration/actors/wfil_token_actor/src/lib.rs b/testing/integration/actors/wfil_token_actor/src/lib.rs new file mode 100644 index 00000000..fe4715c5 --- /dev/null +++ b/testing/integration/actors/wfil_token_actor/src/lib.rs @@ -0,0 +1,155 @@ +use anyhow::Result; +use fil_token::blockstore::Blockstore; +use fil_token::runtime::FvmRuntime; +use fil_token::token::types::*; +use fil_token::token::{Token, TokenHelper}; +use fvm_ipld_encoding::{RawBytes, DAG_CBOR}; +use fvm_sdk as sdk; +use fvm_shared::bigint::bigint_ser::BigIntSer; +use fvm_shared::econ::TokenAmount; +use sdk::NO_DATA_BLOCK_ID; + +struct WfilToken { + /// Default token helper impl + util: TokenHelper, +} + +// TODO: Wrapper is unecessary? +// Instead expose a +impl Token for WfilToken { + fn name(&self) -> String { + String::from("Wrapped FIL") + } + + fn symbol(&self) -> String { + String::from("WFIL") + } + + fn total_supply(&self) -> TokenAmount { + self.util.total_supply() + } + + fn balance_of( + &self, + params: fvm_shared::address::Address, + ) -> Result { + self.util.balance_of(params) + } + + fn increase_allowance(&self, params: ChangeAllowanceParams) -> Result { + self.util.increase_allowance(params) + } + + fn decrease_allowance(&self, params: ChangeAllowanceParams) -> Result { + self.util.decrease_allowance(params) + } + + fn revoke_allowance(&self, params: RevokeAllowanceParams) -> Result { + self.util.revoke_allowance(params) + } + + fn allowance(&self, params: GetAllowanceParams) -> Result { + self.util.allowance(params) + } + + fn burn(&self, params: BurnParams) -> Result { + self.util.burn(params) + } + + fn transfer_from(&self, params: TransferParams) -> Result { + self.util.transfer_from(params) + } +} + +/// Placeholder invoke for testing +#[no_mangle] +pub fn invoke(params: u32) -> u32 { + // Conduct method dispatch. Handle input parameters and return data. + let method_num = sdk::message::method_number(); + + let token_actor = WfilToken { + util: TokenHelper::new(Blockstore {}, FvmRuntime {}), + }; + + //TODO: this internal dispatch can be pushed as a library function into the fil_token crate + // - it should support a few different calling-conventions + // - it should also handle deserialization of raw_params into the expected IPLD types + let res = match method_num { + // Actor constructor + 1 => constructor(), + // Standard token interface + 2 => { + token_actor.name(); + // TODO: store and return CID + NO_DATA_BLOCK_ID + } + 3 => { + token_actor.symbol(); + // TODO: store and return CID + NO_DATA_BLOCK_ID + } + 4 => { + token_actor.total_supply(); + // TODO: store and return CID + NO_DATA_BLOCK_ID + } + 5 => { + // balance of + let params = sdk::message::params_raw(params).unwrap().1; + let params = RawBytes::new(params); + let params = params.deserialize().unwrap(); + let res = token_actor.balance_of(params).unwrap(); + let res = RawBytes::new(fvm_ipld_encoding::to_vec(&BigIntSer(&res)).unwrap()); + let cid = sdk::ipld::put_block(DAG_CBOR, res.bytes()).unwrap(); + cid + } + 6 => { + // increase allowance + NO_DATA_BLOCK_ID + } + 7 => { + // decrease allowance + NO_DATA_BLOCK_ID + } + 8 => { + // revoke_allowance + NO_DATA_BLOCK_ID + } + 9 => { + // allowance + NO_DATA_BLOCK_ID + } + 10 => { + // burn + NO_DATA_BLOCK_ID + } + 11 => { + // transfer_from + NO_DATA_BLOCK_ID + } + // Custom actor interface + 12 => { + // Mint + let params = MintParams { + initial_holder: sdk::message::caller(), + value: sdk::message::value_received(), + }; + let res = token_actor.util.mint(params).unwrap(); + let res = RawBytes::new(fvm_ipld_encoding::to_vec(&res).unwrap()); + let cid = sdk::ipld::put_block(DAG_CBOR, res.bytes()).unwrap(); + cid + } + _ => { + sdk::vm::abort( + fvm_shared::error::ExitCode::USR_ILLEGAL_ARGUMENT.value(), + Some("Unknown method number"), + ); + } + }; + + res +} + +fn constructor() -> u32 { + 0_u32 +} From cbeccf8784aaa146981c35276595a51b73bd292c Mon Sep 17 00:00:00 2001 From: Alex Su Date: Tue, 12 Jul 2022 00:26:38 +1000 Subject: [PATCH 11/21] non-running integration tests --- Cargo.toml | 4 +- fil_token/Cargo.toml | 5 +- testing/fil_token_integration/Cargo.toml | 11 ++++ .../actors/wfil_token_actor/Cargo.toml | 4 +- .../actors/wfil_token_actor/README.md | 0 .../actors/wfil_token_actor/build.rs | 0 .../actors/wfil_token_actor/src/lib.rs | 0 .../src/lib.rs | 0 testing/fil_token_integration/tests/lib.rs | 60 +++++++++++++++++++ testing/integration/Cargo.toml | 4 -- 10 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 testing/fil_token_integration/Cargo.toml rename testing/{integration => fil_token_integration}/actors/wfil_token_actor/Cargo.toml (64%) rename testing/{integration => fil_token_integration}/actors/wfil_token_actor/README.md (100%) rename testing/{integration => fil_token_integration}/actors/wfil_token_actor/build.rs (100%) rename testing/{integration => fil_token_integration}/actors/wfil_token_actor/src/lib.rs (100%) rename testing/{integration => fil_token_integration}/src/lib.rs (100%) create mode 100644 testing/fil_token_integration/tests/lib.rs delete mode 100644 testing/integration/Cargo.toml diff --git a/Cargo.toml b/Cargo.toml index a4aee29a..cec2baab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,6 @@ members = [ "fvm_dispatch", "fil_token", - "testing/integration", - "testing/integration/actors/wfil_token_actor", + "testing/fil_token_integration", + "testing/fil_token_integration/actors/wfil_token_actor", ] \ No newline at end of file diff --git a/fil_token/Cargo.toml b/fil_token/Cargo.toml index 3e3f7575..9cc5bb48 100644 --- a/fil_token/Cargo.toml +++ b/fil_token/Cargo.toml @@ -6,12 +6,11 @@ edition = "2021" [dependencies] anyhow = "1.0.56" cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } -fvm_dispatch = { version = "0.1.0", path = "../fvm_dispatch" } fvm_ipld_blockstore = "0.1.1" fvm_ipld_hamt = "0.5.1" fvm_ipld_amt = { version = "0.4.2", features = ["go-interop"] } fvm_ipld_encoding = "0.2.2" -fvm_sdk = { version = "1.0.0" } -fvm_shared = { version = "0.8.0" } +fvm_sdk = { version = "2.0.0-alpha.1", git = "https://github.com/filecoin-project/ref-fvm" } +fvm_shared = { version = "0.8.0", git = "https://github.com/filecoin-project/ref-fvm" } serde = { version = "1.0.136", features = ["derive"] } serde_tuple = { version = "0.5.0" } \ No newline at end of file diff --git a/testing/fil_token_integration/Cargo.toml b/testing/fil_token_integration/Cargo.toml new file mode 100644 index 00000000..1e62c3be --- /dev/null +++ b/testing/fil_token_integration/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "fil_token_integration_tests" +version = "0.1.0" +repository = "https://github.com/helix-collective/filecoin" +edition = "2021" + +[dependencies] +fil_token = { version = "0.1.0", path = "../../fil_token" } +fvm_integration_tests = { version = "0.1.0", git = "https://github.com/filecoin-project/ref-fvm" } +fvm_ipld_blockstore = { version = "0.1.1", git = "https://github.com/filecoin-project/ref-fvm" } +fvm_shared = { version = "0.8.0", git = "https://github.com/filecoin-project/ref-fvm" } \ No newline at end of file diff --git a/testing/integration/actors/wfil_token_actor/Cargo.toml b/testing/fil_token_integration/actors/wfil_token_actor/Cargo.toml similarity index 64% rename from testing/integration/actors/wfil_token_actor/Cargo.toml rename to testing/fil_token_integration/actors/wfil_token_actor/Cargo.toml index e4f5a34d..b1fb531c 100644 --- a/testing/integration/actors/wfil_token_actor/Cargo.toml +++ b/testing/fil_token_integration/actors/wfil_token_actor/Cargo.toml @@ -7,8 +7,8 @@ edition = "2021" [dependencies] anyhow = { version = "1.0.56" } fvm_ipld_encoding = { version = "0.2.2" } -fvm_sdk = { version = "1.0.0 "} -fvm_shared = { version = "0.8.0" } +fvm_sdk = { version = "2.0.0-alpha.1", git = "https://github.com/filecoin-project/ref-fvm" } +fvm_shared = { version = "0.8.0", git = "https://github.com/filecoin-project/ref-fvm" } fil_token = { version = "0.1.0", path = "../../../../fil_token" } [build-dependencies] diff --git a/testing/integration/actors/wfil_token_actor/README.md b/testing/fil_token_integration/actors/wfil_token_actor/README.md similarity index 100% rename from testing/integration/actors/wfil_token_actor/README.md rename to testing/fil_token_integration/actors/wfil_token_actor/README.md diff --git a/testing/integration/actors/wfil_token_actor/build.rs b/testing/fil_token_integration/actors/wfil_token_actor/build.rs similarity index 100% rename from testing/integration/actors/wfil_token_actor/build.rs rename to testing/fil_token_integration/actors/wfil_token_actor/build.rs diff --git a/testing/integration/actors/wfil_token_actor/src/lib.rs b/testing/fil_token_integration/actors/wfil_token_actor/src/lib.rs similarity index 100% rename from testing/integration/actors/wfil_token_actor/src/lib.rs rename to testing/fil_token_integration/actors/wfil_token_actor/src/lib.rs diff --git a/testing/integration/src/lib.rs b/testing/fil_token_integration/src/lib.rs similarity index 100% rename from testing/integration/src/lib.rs rename to testing/fil_token_integration/src/lib.rs diff --git a/testing/fil_token_integration/tests/lib.rs b/testing/fil_token_integration/tests/lib.rs new file mode 100644 index 00000000..502eec10 --- /dev/null +++ b/testing/fil_token_integration/tests/lib.rs @@ -0,0 +1,60 @@ +use std::env; + +use fil_token::blockstore::Blockstore as ActorBlockstore; +use fil_token::token::state::TokenState; +use fvm_integration_tests::tester::{Account, Tester}; +use fvm_ipld_blockstore::MemoryBlockstore; +use fvm_shared::address::Address; +use fvm_shared::bigint::Zero; +use fvm_shared::econ::TokenAmount; +use fvm_shared::message::Message; +use fvm_shared::state::StateTreeVersion; +use fvm_shared::version::NetworkVersion; + +const WFIL_TOKEN_WASM_COMPILED_PATH: &str = + "../../target/debug/wbuild/wfil_token_actor/wfil_token_actor.compact.wasm"; + +#[test] +fn mint_tokens() { + let mut tester = Tester::new( + NetworkVersion::V15, + StateTreeVersion::V4, + MemoryBlockstore::default(), + ) + .unwrap(); + + let minter: [Account; 1] = tester.create_accounts().unwrap(); + + // Get wasm bin + let wasm_path = env::current_dir() + .unwrap() + .join(WFIL_TOKEN_WASM_COMPILED_PATH) + .canonicalize() + .unwrap(); + let wasm_bin = std::fs::read(wasm_path).expect("Unable to read file"); + + let actor_blockstore = ActorBlockstore::default(); + let actor_state = TokenState::new(&actor_blockstore, "Wrapped FIL", "WFIL").unwrap(); + let state_cid = tester.set_state(&actor_state).unwrap(); + + // let actor_address = Address::new_id(10000); + // tester.set_actor_from_bin(&wasm_bin, state_cid, actor_address, TokenAmount::zero()); + + // // Instantiate machine + // tester.instantiate_machine().unwrap(); + + // let message = Message { + // from: minter[0].1, + // to: actor_address, + // gas_limit: 10000000, + // method_num: 12, // 12 is Mint function + // value: TokenAmount::from(150), + // ..Message::default() + // }; + + // let res = tester + // .executor + // .unwrap() + // .execute_message(message, ApplyKind::Explicit, 100) + // .unwrap(); +} diff --git a/testing/integration/Cargo.toml b/testing/integration/Cargo.toml deleted file mode 100644 index eb3eea0f..00000000 --- a/testing/integration/Cargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[package] -name = "fvm_integration_tests" -version = "0.1.0" -repository = "https://github.com/helix-collective/filecoin" \ No newline at end of file From 870b3ff07888efd2d7cfe9814e4b59d7249ab051 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Tue, 12 Jul 2022 09:50:40 +1000 Subject: [PATCH 12/21] clippy fixes --- fil_token/src/token/errors.rs | 2 +- fil_token/src/token/mod.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/fil_token/src/token/errors.rs b/fil_token/src/token/errors.rs index 19e45f94..c74b6f09 100644 --- a/fil_token/src/token/errors.rs +++ b/fil_token/src/token/errors.rs @@ -11,7 +11,7 @@ pub enum RuntimeError { impl Display for RuntimeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - RuntimeError::AddrNotFound(_) => write!(f, "Address not found: {}", self), + RuntimeError::AddrNotFound(_) => write!(f, "Address not found"), } } } diff --git a/fil_token/src/token/mod.rs b/fil_token/src/token/mod.rs index c54127ce..0b974e19 100644 --- a/fil_token/src/token/mod.rs +++ b/fil_token/src/token/mod.rs @@ -181,7 +181,7 @@ where let caller_id = self.fvm.caller(); let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id); - let spender = self.fvm.resolve_address(&¶ms.spender)?; + let spender = self.fvm.resolve_address(¶ms.spender)?; let new_amount = match caller_allowances_map.get(&spender)? { // Allowance exists - attempt to calculate new allowance Some(existing_allowance) => match existing_allowance.0.checked_add(¶ms.value) { @@ -214,7 +214,7 @@ where let caller_id = self.fvm.caller(); let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id); - let spender = self.fvm.resolve_address(&¶ms.spender)?; + let spender = self.fvm.resolve_address(¶ms.spender)?; let new_allowance = match caller_allowances_map.get(&spender)? { Some(existing_allowance) => { @@ -252,7 +252,7 @@ where let caller_id = self.fvm.caller(); let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id); - let spender = self.fvm.resolve_address(&¶ms.spender)?; + let spender = self.fvm.resolve_address(¶ms.spender)?; let new_allowance = TokenAmount::zero(); caller_allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; @@ -272,7 +272,7 @@ where let owner = self.fvm.resolve_address(¶ms.owner)?; let owner_allowances_map = state.get_actor_allowance_map(&self.bs, owner); - let spender = self.fvm.resolve_address(&¶ms.spender)?; + let spender = self.fvm.resolve_address(¶ms.spender)?; let allowance = match owner_allowances_map.get(&spender)? { Some(allowance) => allowance.0.clone(), From dc3eb694b65322b77ef96f2db05f3bdbbc05d063 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Tue, 12 Jul 2022 11:42:09 +1000 Subject: [PATCH 13/21] revert dependency overrides --- fil_token/Cargo.toml | 4 ++-- fil_token/src/token/types.rs | 2 +- .../fil_token_integration/actors/wfil_token_actor/Cargo.toml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/fil_token/Cargo.toml b/fil_token/Cargo.toml index 9cc5bb48..cc4d1673 100644 --- a/fil_token/Cargo.toml +++ b/fil_token/Cargo.toml @@ -10,7 +10,7 @@ fvm_ipld_blockstore = "0.1.1" fvm_ipld_hamt = "0.5.1" fvm_ipld_amt = { version = "0.4.2", features = ["go-interop"] } fvm_ipld_encoding = "0.2.2" -fvm_sdk = { version = "2.0.0-alpha.1", git = "https://github.com/filecoin-project/ref-fvm" } -fvm_shared = { version = "0.8.0", git = "https://github.com/filecoin-project/ref-fvm" } +fvm_sdk = { version = "1.0.0" } +fvm_shared = { version = "0.8.0" } serde = { version = "1.0.136", features = ["derive"] } serde_tuple = { version = "0.5.0" } \ No newline at end of file diff --git a/fil_token/src/token/types.rs b/fil_token/src/token/types.rs index 4e0bf61d..3fabd1d7 100644 --- a/fil_token/src/token/types.rs +++ b/fil_token/src/token/types.rs @@ -1,4 +1,4 @@ -use fvm_ipld_encoding::tuple::*; +use fvm_ipld_encoding::tuple::{Deserialize_tuple, Serialize_tuple}; use fvm_ipld_encoding::{Cbor, RawBytes}; use fvm_shared::address::Address; use fvm_shared::bigint::bigint_ser; diff --git a/testing/fil_token_integration/actors/wfil_token_actor/Cargo.toml b/testing/fil_token_integration/actors/wfil_token_actor/Cargo.toml index b1fb531c..4b7f43ee 100644 --- a/testing/fil_token_integration/actors/wfil_token_actor/Cargo.toml +++ b/testing/fil_token_integration/actors/wfil_token_actor/Cargo.toml @@ -7,8 +7,8 @@ edition = "2021" [dependencies] anyhow = { version = "1.0.56" } fvm_ipld_encoding = { version = "0.2.2" } -fvm_sdk = { version = "2.0.0-alpha.1", git = "https://github.com/filecoin-project/ref-fvm" } -fvm_shared = { version = "0.8.0", git = "https://github.com/filecoin-project/ref-fvm" } +fvm_sdk = { version = "1.0.0" } +fvm_shared = { version = "0.8.0" } fil_token = { version = "0.1.0", path = "../../../../fil_token" } [build-dependencies] From f7efed9f5cbff07702c4020db52be2eb1a185836 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Thu, 14 Jul 2022 11:19:14 +1000 Subject: [PATCH 14/21] add docs; separate api interface from library interface --- fil_token/src/runtime/mod.rs | 4 + fil_token/src/token/mod.rs | 76 +++++++++-------- fil_token/src/token/state.rs | 84 +++++++------------ fil_token/src/token/types.rs | 8 -- .../actors/wfil_token_actor/src/lib.rs | 4 +- testing/fil_token_integration/tests/lib.rs | 22 ++--- 6 files changed, 86 insertions(+), 112 deletions(-) diff --git a/fil_token/src/runtime/mod.rs b/fil_token/src/runtime/mod.rs index 88378d02..4b5d5231 100644 --- a/fil_token/src/runtime/mod.rs +++ b/fil_token/src/runtime/mod.rs @@ -4,8 +4,12 @@ pub use fvm::*; use anyhow::Result; use fvm_shared::address::Address; +/// Abstraction of the runtime that an actor is executed in, providing access to syscalls and +/// features of the FVM pub trait Runtime { + /// Get the direct-caller that invoked the current actor fn caller(&self) -> u64; + /// Attempts to resolve an address to an ActorID fn resolve_address(&self, addr: &Address) -> Result; } diff --git a/fil_token/src/token/mod.rs b/fil_token/src/token/mod.rs index 0b974e19..addd84ec 100644 --- a/fil_token/src/token/mod.rs +++ b/fil_token/src/token/mod.rs @@ -10,6 +10,7 @@ use crate::runtime::Runtime; use anyhow::bail; use anyhow::Result; use fvm_ipld_blockstore::Blockstore as IpldStore; +use fvm_sdk::sself; use fvm_shared::address::Address; use fvm_shared::bigint::bigint_ser::BigIntDe; use fvm_shared::bigint::BigInt; @@ -63,7 +64,7 @@ pub trait Token { fn burn(&self, params: BurnParams) -> Result; /// Transfer between two addresses - fn transfer_from(&self, params: TransferParams) -> Result; + fn transfer(&self, params: TransferParams) -> Result; } /// Holds injectable services to access/interface with IPLD/FVM layer @@ -88,8 +89,8 @@ where } /// Constructs the token - pub fn init_state(&self, params: ConstructorParams) -> Result<()> { - let init_state = TokenState::new(&self.bs, ¶ms.name, ¶ms.symbol)?; + pub fn init_state(&self) -> Result<()> { + let init_state = TokenState::new(&self.bs)?; init_state.save(&self.bs); Ok(()) } @@ -100,8 +101,11 @@ where // - or that other (TBD) minting rules are satified // these should be injectable by the token author - let mut state = self.load_state(); - let mut balances = state.get_balance_map(&self.bs); + // TODO: proper error handling here + let state = self.load_state(); + let mut state = state.unwrap(); + let balances = state.get_balance_map(&self.bs); + let mut balances = balances.unwrap(); // Mint the tokens into a specified account let balance = balances @@ -130,35 +134,33 @@ where } /// Helper function that loads the root of the state tree related to token-accounting - fn load_state(&self) -> TokenState { - TokenState::load(&self.bs) + fn load_state(&self) -> Result { + // TODO: replace sself usage with abstraction + TokenState::load(&self.bs, &sself::root().unwrap()) } } -impl Token for TokenHelper +impl TokenHelper where BS: IpldStore + Copy, FVM: Runtime, { - fn name(&self) -> String { - let state = self.load_state(); - state.name - } - - fn symbol(&self) -> String { - let state = self.load_state(); - state.symbol - } - - fn total_supply(&self) -> TokenAmount { - let state = self.load_state(); + /// Gets the total number of tokens in existence. + /// + /// This equals the sum of `balance_of` called on all addresses. This equals sum of all + /// successful `mint` calls minus the sum of all successful `burn`/`burn_from` calls + pub fn total_supply(&self) -> TokenAmount { + let state = self.load_state().unwrap(); state.supply } - fn balance_of(&self, holder: Address) -> Result { + /// Returns the balance associated with a particular address + /// + /// + pub fn balance_of(&self, holder: Address) -> Result { // Load the HAMT holding balances - let state = self.load_state(); - let balances = state.get_balance_map(&self.bs); + let state = self.load_state().unwrap(); + let balances = state.get_balance_map(&self.bs).unwrap(); // Resolve the address let addr_id = self.fvm.resolve_address(&holder)?; @@ -174,12 +176,12 @@ where } } - fn increase_allowance(&self, params: ChangeAllowanceParams) -> Result { + pub fn increase_allowance(&self, params: ChangeAllowanceParams) -> Result { // Load the HAMT holding balances - let state = self.load_state(); + let state = self.load_state().unwrap(); let caller_id = self.fvm.caller(); - let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id); + let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id).unwrap(); let spender = self.fvm.resolve_address(¶ms.spender)?; let new_amount = match caller_allowances_map.get(&spender)? { @@ -207,12 +209,12 @@ where }) } - fn decrease_allowance(&self, params: ChangeAllowanceParams) -> Result { + pub fn decrease_allowance(&self, params: ChangeAllowanceParams) -> Result { // Load the HAMT holding balances - let state = self.load_state(); + let state = self.load_state().unwrap(); let caller_id = self.fvm.caller(); - let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id); + let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id).unwrap(); let spender = self.fvm.resolve_address(¶ms.spender)?; @@ -245,12 +247,12 @@ where }) } - fn revoke_allowance(&self, params: RevokeAllowanceParams) -> Result { + pub fn revoke_allowance(&self, params: RevokeAllowanceParams) -> Result { // Load the HAMT holding balances - let state = self.load_state(); + let state = self.load_state().unwrap(); let caller_id = self.fvm.caller(); - let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id); + let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id).unwrap(); let spender = self.fvm.resolve_address(¶ms.spender)?; let new_allowance = TokenAmount::zero(); @@ -265,12 +267,12 @@ where }) } - fn allowance(&self, params: GetAllowanceParams) -> Result { + pub fn allowance(&self, params: GetAllowanceParams) -> Result { // Load the HAMT holding balances - let state = self.load_state(); + let state = self.load_state().unwrap(); let owner = self.fvm.resolve_address(¶ms.owner)?; - let owner_allowances_map = state.get_actor_allowance_map(&self.bs, owner); + let owner_allowances_map = state.get_actor_allowance_map(&self.bs, owner).unwrap(); let spender = self.fvm.resolve_address(¶ms.spender)?; @@ -286,11 +288,11 @@ where }) } - fn burn(&self, _params: BurnParams) -> Result { + pub fn burn(&self, _params: BurnParams) -> Result { todo!() } - fn transfer_from(&self, _params: TransferParams) -> Result { + pub fn transfer(&self, _params: TransferParams) -> Result { todo!() } } diff --git a/fil_token/src/token/state.rs b/fil_token/src/token/state.rs index 47ccfac7..f68770c9 100644 --- a/fil_token/src/token/state.rs +++ b/fil_token/src/token/state.rs @@ -17,26 +17,12 @@ use fvm_shared::econ::TokenAmount; use fvm_shared::ActorID; use fvm_shared::HAMT_BIT_WIDTH; -/// A macro to abort concisely. -macro_rules! abort { - ($code:ident, $msg:literal $(, $ex:expr)*) => { - fvm_sdk::vm::abort( - fvm_shared::error::ExitCode::$code.value(), - Some(format!($msg, $($ex,)*).as_str()), - ) - }; -} - /// Token state ipld structure #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] pub struct TokenState { /// Total supply of token #[serde(with = "bigint_ser")] pub supply: TokenAmount, - /// Token name - pub name: String, - /// Ticker symbol - pub symbol: String, /// Map of balances as a Hamt pub balances: Cid, @@ -46,7 +32,7 @@ pub struct TokenState { /// Functions to get and modify token state to and from the IPLD layer impl TokenState { - pub fn new(store: &BS, name: &str, symbol: &str) -> Result { + pub fn new(store: &BS) -> Result { let empty_balance_map = Hamt::<_, ()>::new_with_bit_width(store, HAMT_BIT_WIDTH) .flush() .map_err(|e| anyhow!("Failed to create empty balances map state {}", e))?; @@ -56,35 +42,26 @@ impl TokenState { Ok(Self { supply: Default::default(), - name: name.to_string(), - symbol: symbol.to_string(), balances: empty_balance_map, allowances: empty_allowances_map, }) } - /// Loads a fresh copy of the state from a blockstore - pub fn load(bs: &BS) -> Self { - // First, load the current state root - // TODO: inject sself capabilities - let root = match sself::root() { - Ok(root) => root, - Err(err) => abort!(USR_ILLEGAL_STATE, "failed to get root: {:?}", err), - }; - + /// Loads a fresh copy of the state from a blockstore from a given cid + pub fn load(bs: &BS, cid: &Cid) -> Result { // Load the actor state from the state tree. - match bs.get_cbor::(&root) { - Ok(Some(state)) => state, - Ok(None) => abort!(USR_ILLEGAL_STATE, "state does not exist"), - Err(err) => abort!(USR_ILLEGAL_STATE, "failed to get state: {}", err), + match bs.get_cbor::(&cid) { + Ok(Some(state)) => Ok(state), + Ok(None) => Err(anyhow!("No state at this cid {:?}", cid)), + Err(err) => Err(anyhow!("failed to get state: {}", err)), } } /// Saves the current state to the blockstore - pub fn save(&self, bs: &BS) -> Cid { + pub fn save(&self, bs: &BS) -> Result { let serialized = match fvm_ipld_encoding::to_vec(self) { Ok(s) => s, - Err(err) => abort!(USR_SERIALIZATION, "failed to serialize state: {:?}", err), + Err(err) => return Err(anyhow!("failed to serialize state: {:?}", err)), }; let block = Block { codec: DAG_CBOR, @@ -92,46 +69,49 @@ impl TokenState { }; let cid = match bs.put(Code::Blake2b256, &block) { Ok(cid) => cid, - Err(err) => abort!(USR_SERIALIZATION, "failed to store initial state: {:}", err), + Err(err) => return Err(anyhow!("failed to store initial state: {:}", err)), }; if let Err(err) = sself::set_root(&cid) { - abort!(USR_ILLEGAL_STATE, "failed to set root ciid: {:}", err); + return Err(anyhow!("failed to set root ciid: {:}", err)); } - cid + Ok(cid) } - pub fn get_balance_map(&self, bs: &BS) -> Hamt { + pub fn get_balance_map( + &self, + bs: &BS, + ) -> Result> { match Hamt::::load(&self.balances, *bs) { - Ok(map) => map, - Err(err) => abort!(USR_ILLEGAL_STATE, "Failed to load balances hamt: {:?}", err), + Ok(map) => Ok(map), + Err(err) => return Err(anyhow!("Failed to load balances hamt: {:?}", err)), } } /// Get the global allowances map /// /// Gets a HAMT with CIDs linking to other HAMTs - pub fn get_allowances_map(&self, bs: &BS) -> Hamt { + pub fn get_allowances_map( + &self, + bs: &BS, + ) -> Result> { match Hamt::::load(&self.allowances, *bs) { - Ok(map) => map, - Err(err) => abort!( - USR_ILLEGAL_STATE, - "Failed to load allowances hamt: {:?}", - err - ), + Ok(map) => Ok(map), + Err(err) => return Err(anyhow!("Failed to load allowances hamt: {:?}", err)), } } /// Get the allowances map of a specific actor, lazily creating one if it didn't exist + /// TODO: don't lazily create this, higher logic needed to get allowances etc. pub fn get_actor_allowance_map( &self, bs: &BS, owner: ActorID, - ) -> Hamt { - let mut global_allowances = self.get_allowances_map(bs); + ) -> Result> { + let mut global_allowances = self.get_allowances_map(bs).unwrap(); match global_allowances.get(&owner) { Ok(Some(map)) => { // authorising actor already had an allowance map, return it - Hamt::::load(map, *bs).unwrap() + Ok(Hamt::::load(map, *bs).unwrap()) } Ok(None) => { // authorising actor does not have an allowance map, create one and return it @@ -141,13 +121,9 @@ impl TokenState { .map_err(|e| anyhow!("Failed to create empty balances map state {}", e)) .unwrap(); global_allowances.set(owner, cid).unwrap(); - new_actor_allowances + Ok(new_actor_allowances) } - Err(e) => abort!( - USR_ILLEGAL_STATE, - "failed to get actor's allowance map {:?}", - e - ), + Err(e) => Err(anyhow!("failed to get actor's allowance map {:?}", e)), } } diff --git a/fil_token/src/token/types.rs b/fil_token/src/token/types.rs index 3fabd1d7..133604fe 100644 --- a/fil_token/src/token/types.rs +++ b/fil_token/src/token/types.rs @@ -5,14 +5,6 @@ use fvm_shared::bigint::bigint_ser; use fvm_shared::econ::TokenAmount; use fvm_shared::ActorID; -#[derive(Serialize_tuple, Deserialize_tuple)] -pub struct ConstructorParams { - pub mint_params: MintParams, - pub name: String, - pub symbol: String, -} - -/// Called during construction of the token actor to set a supply #[derive(Serialize_tuple, Deserialize_tuple)] pub struct MintParams { pub initial_holder: ActorID, diff --git a/testing/fil_token_integration/actors/wfil_token_actor/src/lib.rs b/testing/fil_token_integration/actors/wfil_token_actor/src/lib.rs index fe4715c5..65db2888 100644 --- a/testing/fil_token_integration/actors/wfil_token_actor/src/lib.rs +++ b/testing/fil_token_integration/actors/wfil_token_actor/src/lib.rs @@ -56,8 +56,8 @@ impl Token for WfilToken { self.util.burn(params) } - fn transfer_from(&self, params: TransferParams) -> Result { - self.util.transfer_from(params) + fn transfer(&self, params: TransferParams) -> Result { + self.util.transfer(params) } } diff --git a/testing/fil_token_integration/tests/lib.rs b/testing/fil_token_integration/tests/lib.rs index 502eec10..806ac09f 100644 --- a/testing/fil_token_integration/tests/lib.rs +++ b/testing/fil_token_integration/tests/lib.rs @@ -25,17 +25,17 @@ fn mint_tokens() { let minter: [Account; 1] = tester.create_accounts().unwrap(); - // Get wasm bin - let wasm_path = env::current_dir() - .unwrap() - .join(WFIL_TOKEN_WASM_COMPILED_PATH) - .canonicalize() - .unwrap(); - let wasm_bin = std::fs::read(wasm_path).expect("Unable to read file"); - - let actor_blockstore = ActorBlockstore::default(); - let actor_state = TokenState::new(&actor_blockstore, "Wrapped FIL", "WFIL").unwrap(); - let state_cid = tester.set_state(&actor_state).unwrap(); + // // Get wasm bin + // let wasm_path = env::current_dir() + // .unwrap() + // .join(WFIL_TOKEN_WASM_COMPILED_PATH) + // .canonicalize() + // .unwrap(); + // let wasm_bin = std::fs::read(wasm_path).expect("Unable to read file"); + + // let actor_blockstore = ActorBlockstore::default(); + // let actor_state = TokenState::new(&actor_blockstore).unwrap(); + // let state_cid = tester.set_state(&actor_state).unwrap(); // let actor_address = Address::new_id(10000); // tester.set_actor_from_bin(&wasm_bin, state_cid, actor_address, TokenAmount::zero()); From 94efb15d9fec802513f2f8806d8a27ceffbcd872 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Tue, 12 Jul 2022 14:41:22 +1000 Subject: [PATCH 15/21] wip: test --- fil_token/tests/blockstore.rs | 32 ++++++++++++++++++++++++++++++++ fil_token/tests/lib.rs | 11 +++++++++++ fil_token/tests/runtime.rs | 22 ++++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 fil_token/tests/blockstore.rs create mode 100644 fil_token/tests/lib.rs create mode 100644 fil_token/tests/runtime.rs diff --git a/fil_token/tests/blockstore.rs b/fil_token/tests/blockstore.rs new file mode 100644 index 00000000..e5779b38 --- /dev/null +++ b/fil_token/tests/blockstore.rs @@ -0,0 +1,32 @@ +use std::{cell::RefCell, collections::HashMap}; + +use anyhow::Result; +use cid::Cid; +use fvm_ipld_blockstore::Blockstore; + +/// An in-memory blockstore impl taken from filecoin-project/ref-fvm +#[derive(Debug, Default, Clone)] +pub struct MemoryBlockstore { + blocks: RefCell>>, +} + +impl MemoryBlockstore { + pub fn new() -> Self { + Self::default() + } +} + +impl Blockstore for MemoryBlockstore { + fn has(&self, k: &Cid) -> Result { + Ok(self.blocks.borrow().contains_key(k)) + } + + fn get(&self, k: &Cid) -> Result>> { + Ok(self.blocks.borrow().get(k).cloned()) + } + + fn put_keyed(&self, k: &Cid, block: &[u8]) -> Result<()> { + self.blocks.borrow_mut().insert(*k, block.into()); + Ok(()) + } +} diff --git a/fil_token/tests/lib.rs b/fil_token/tests/lib.rs new file mode 100644 index 00000000..b6721eb6 --- /dev/null +++ b/fil_token/tests/lib.rs @@ -0,0 +1,11 @@ +mod blockstore; +mod runtime; +use blockstore::MemoryBlockstore; +use runtime::TestRuntime; + +use fil_token::token::{Token, TokenHelper}; + +#[test] +fn it_mints() { + let token = TokenHelper::new(MemoryBlockstore::new(), TestRuntime::new(1)); +} diff --git a/fil_token/tests/runtime.rs b/fil_token/tests/runtime.rs new file mode 100644 index 00000000..4aefab98 --- /dev/null +++ b/fil_token/tests/runtime.rs @@ -0,0 +1,22 @@ +use std::intrinsics::const_allocate; + +use fil_token::runtime::Runtime; +pub struct TestRuntime { + caller: u64, +} + +impl TestRuntime { + pub fn new(caller: u64) -> Self { + Self { caller } + } +} + +impl Runtime for TestRuntime { + fn caller(&self) -> u64 { + return self.caller; + } + + fn resolve_address(&self, addr: &fvm_shared::address::Address) -> anyhow::Result { + Ok(addr.id().unwrap()) + } +} From e686791c37c9762fe53ec90422a270c79bb18572 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Thu, 14 Jul 2022 18:48:36 +1000 Subject: [PATCH 16/21] hide ipld data structures behind the state abstraction --- fil_token/Cargo.toml | 3 +- fil_token/src/blockstore.rs | 2 + fil_token/src/runtime/fvm.rs | 1 + fil_token/src/token/mod.rs | 350 ++++++++---------- fil_token/src/token/state.rs | 110 +++++- fil_token/src/token/types.rs | 88 +++++ fil_token/tests/lib.rs | 4 +- fil_token/tests/runtime.rs | 2 - .../actors/wfil_token_actor/src/lib.rs | 45 ++- 9 files changed, 377 insertions(+), 228 deletions(-) diff --git a/fil_token/Cargo.toml b/fil_token/Cargo.toml index cc4d1673..ea05d265 100644 --- a/fil_token/Cargo.toml +++ b/fil_token/Cargo.toml @@ -13,4 +13,5 @@ fvm_ipld_encoding = "0.2.2" fvm_sdk = { version = "1.0.0" } fvm_shared = { version = "0.8.0" } serde = { version = "1.0.136", features = ["derive"] } -serde_tuple = { version = "0.5.0" } \ No newline at end of file +serde_tuple = { version = "0.5.0" } +thiserror = { version = "1.0.31" } \ No newline at end of file diff --git a/fil_token/src/blockstore.rs b/fil_token/src/blockstore.rs index 2042bbea..c579b914 100644 --- a/fil_token/src/blockstore.rs +++ b/fil_token/src/blockstore.rs @@ -1,3 +1,5 @@ +//! Blockstore implementation is borrowed from https://github.com/filecoin-project/builtin-actors/blob/6df845dcdf9872beb6e871205eb34dcc8f7550b5/runtime/src/runtime/actor_blockstore.rs +//! This impl will likely be made redundant if low-level SDKs export blockstore implementations use std::convert::TryFrom; use anyhow::{anyhow, Result}; diff --git a/fil_token/src/runtime/fvm.rs b/fil_token/src/runtime/fvm.rs index 72483ca9..3bac81b1 100644 --- a/fil_token/src/runtime/fvm.rs +++ b/fil_token/src/runtime/fvm.rs @@ -5,6 +5,7 @@ use fvm_sdk as sdk; use sdk::actor; use sdk::message; +/// Provides access to the FVM which acts as the runtime for actors deployed on-chain pub struct FvmRuntime {} impl Runtime for FvmRuntime { diff --git a/fil_token/src/token/mod.rs b/fil_token/src/token/mod.rs index addd84ec..51ae07c4 100644 --- a/fil_token/src/token/mod.rs +++ b/fil_token/src/token/mod.rs @@ -1,73 +1,26 @@ pub mod errors; pub mod receiver; pub mod state; -pub mod types; +mod types; use self::errors::ActorError; use self::state::TokenState; -use self::types::*; +pub use self::types::*; use crate::runtime::Runtime; use anyhow::bail; +use anyhow::Ok; use anyhow::Result; +use cid::Cid; use fvm_ipld_blockstore::Blockstore as IpldStore; -use fvm_sdk::sself; -use fvm_shared::address::Address; use fvm_shared::bigint::bigint_ser::BigIntDe; use fvm_shared::bigint::BigInt; use fvm_shared::bigint::Zero; use fvm_shared::econ::TokenAmount; +use fvm_shared::ActorID; -/// A macro to abort concisely. -macro_rules! abort { - ($code:ident, $msg:literal $(, $ex:expr)*) => { - fvm_sdk::vm::abort( - fvm_shared::error::ExitCode::$code.value(), - Some(format!($msg, $($ex,)*).as_str()), - ) - }; -} - -/// A standard fungible token interface allowing for on-chain transactions -pub trait Token { - /// Returns the name of the token - fn name(&self) -> String; - - /// Returns the ticker symbol of the token - fn symbol(&self) -> String; - - /// Returns the total amount of the token in existence - fn total_supply(&self) -> TokenAmount; - - /// Gets the balance of a particular address (if it exists). - fn balance_of(&self, params: Address) -> Result; - - /// Atomically increase the amount that a spender can pull from an account - /// - /// Returns the new allowance between those two addresses - fn increase_allowance(&self, params: ChangeAllowanceParams) -> Result; - - /// Atomically decrease the amount that a spender can pull from an account - /// - /// The allowance cannot go below 0 and will be capped if the requested decrease - /// is more than the current allowance - fn decrease_allowance(&self, params: ChangeAllowanceParams) -> Result; - - /// Revoke the allowance between two addresses by setting it to 0 - fn revoke_allowance(&self, params: RevokeAllowanceParams) -> Result; - - /// Get the allowance between two addresses - /// - /// The spender can burn or transfer the allowance amount out of the owner's address - fn allowance(&self, params: GetAllowanceParams) -> Result; - - /// Burn tokens from a specified account, decreasing the total supply - fn burn(&self, params: BurnParams) -> Result; - - /// Transfer between two addresses - fn transfer(&self, params: TransferParams) -> Result; -} - -/// Holds injectable services to access/interface with IPLD/FVM layer +/// Library functions that implement core FRC-??? standards +/// +/// Holds injectable services to access/interface with IPLD/FVM layer. pub struct TokenHelper where BS: IpldStore + Copy, @@ -76,7 +29,9 @@ where /// Injected blockstore bs: BS, /// Access to the runtime - fvm: FVM, + _runtime: FVM, + /// Root of the token state tree + token_state: Cid, } impl TokenHelper @@ -84,68 +39,47 @@ where BS: IpldStore + Copy, FVM: Runtime, { - pub fn new(bs: BS, fvm: FVM) -> Self { - Self { bs, fvm } + /// Instantiate a token helper with access to a blockstore and runtime + pub fn new(bs: BS, runtime: FVM, token_state: Cid) -> Self { + Self { + bs, + _runtime: runtime, + token_state, + } } - /// Constructs the token - pub fn init_state(&self) -> Result<()> { + /// Constructs the token state tree and saves it at a CID + pub fn init_state(&self) -> Result { let init_state = TokenState::new(&self.bs)?; - init_state.save(&self.bs); - Ok(()) - } - - // Utility function for token-authors to mint supply - pub fn mint(&self, params: MintParams) -> Result { - // TODO: check we are being called in the constructor by init system actor - // - or that other (TBD) minting rules are satified - - // these should be injectable by the token author - // TODO: proper error handling here - let state = self.load_state(); - let mut state = state.unwrap(); - let balances = state.get_balance_map(&self.bs); - let mut balances = balances.unwrap(); - - // Mint the tokens into a specified account - let balance = balances - .delete(¶ms.initial_holder)? - .map(|de| de.1 .0) - .unwrap_or_else(TokenAmount::zero); - let new_balance = balance - .checked_add(¶ms.value) - .ok_or_else(|| ActorError::Arithmetic(String::from("Minting into caused overflow")))?; - balances.set(params.initial_holder, BigIntDe(new_balance))?; - - // set the global supply of the contract - let new_supply = state.supply.checked_add(¶ms.value).ok_or_else(|| { - ActorError::Arithmetic(String::from("Minting caused total supply to overflow")) - })?; - state.supply = new_supply; - - // commit the state if supply and balance increased - state.save(&self.bs); - - Ok(MintReturn { - newly_minted: params.value, - successful: true, - total_supply: state.supply, - }) + init_state.save(&self.bs) } /// Helper function that loads the root of the state tree related to token-accounting fn load_state(&self) -> Result { - // TODO: replace sself usage with abstraction - TokenState::load(&self.bs, &sself::root().unwrap()) + TokenState::load(&self.bs, &self.token_state) } -} -impl TokenHelper -where - BS: IpldStore + Copy, - FVM: Runtime, -{ - /// Gets the total number of tokens in existence. + /// Mints the specified value of tokens into an account + /// + /// If the total supply or account balance overflows, this method returns an error. The mint + /// amount must be non-negative or the method returns an error. + pub fn mint(&self, initial_holder: ActorID, value: TokenAmount) -> Result<()> { + if value.lt(&TokenAmount::zero()) { + bail!("value of mint was negative {}", value); + } + + // Increase the balance of the actor and increase total supply + let mut state = self.load_state()?; + state.increase_balance(&self.bs, initial_holder, &value)?; + state.increase_supply(&value)?; + + // Commit the state atomically if supply and balance increased + state.save(&self.bs)?; + + Ok(()) + } + + /// Gets the total number of tokens in existence /// /// This equals the sum of `balance_of` called on all addresses. This equals sum of all /// successful `mint` calls minus the sum of all successful `burn`/`burn_from` calls @@ -156,140 +90,170 @@ where /// Returns the balance associated with a particular address /// - /// - pub fn balance_of(&self, holder: Address) -> Result { + /// Accounts that have never received transfers implicitly have a zero-balance + pub fn balance_of(&self, holder: ActorID) -> Result { // Load the HAMT holding balances - let state = self.load_state().unwrap(); - let balances = state.get_balance_map(&self.bs).unwrap(); - - // Resolve the address - let addr_id = self.fvm.resolve_address(&holder)?; - - match balances.get(&addr_id) { - Ok(Some(bal)) => Ok(bal.clone().0), - Ok(None) => Ok(TokenAmount::zero()), - Err(err) => abort!( - USR_ILLEGAL_STATE, - "Failed to get balance from hamt: {:?}", - err - ), - } + let state = self.load_state()?; + state.get_balance(&self.bs, holder) } - pub fn increase_allowance(&self, params: ChangeAllowanceParams) -> Result { - // Load the HAMT holding balances - let state = self.load_state().unwrap(); + /// Increase the allowance that a spender controls of the owner's balance by the requested delta + /// + /// Returns an error if requested delta is negative or there are errors in (de)sereliazation of + /// state. Else returns the new allowance. + pub fn increase_allowance( + &self, + owner: ActorID, + spender: ActorID, + delta: TokenAmount, + ) -> Result { + if delta.lt(&TokenAmount::zero()) { + bail!("value of allowance increase was negative {}", delta); + } - let caller_id = self.fvm.caller(); - let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id).unwrap(); + // Retrieve the HAMT holding balances + let state = self.load_state()?; + let mut owner_allowances = state.get_actor_allowance_map(&self.bs, owner)?; - let spender = self.fvm.resolve_address(¶ms.spender)?; - let new_amount = match caller_allowances_map.get(&spender)? { + let new_amount = match owner_allowances.get(&spender)? { // Allowance exists - attempt to calculate new allowance - Some(existing_allowance) => match existing_allowance.0.checked_add(¶ms.value) { + Some(existing_allowance) => match existing_allowance.0.checked_add(&delta) { Some(new_allowance) => { - caller_allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; + owner_allowances.set(spender, BigIntDe(new_allowance.clone()))?; new_allowance } - None => bail!(ActorError::Arithmetic(String::from("Allowance overflowed"))), + None => bail!(ActorError::Arithmetic(format!( + "allowance overflowed attempting to add {} to existing allowance of {} between {} {}", + delta, existing_allowance.0, owner, spender + ))), }, // No allowance recorded previously None => { - caller_allowances_map.set(spender, BigIntDe(params.value.clone()))?; - params.value + owner_allowances.set(spender, BigIntDe(delta.clone()))?; + delta } }; - state.save(&self.bs); + state.save(&self.bs)?; - Ok(AllowanceReturn { - owner: params.owner, - spender: params.spender, - value: new_amount, - }) + Ok(new_amount) } - pub fn decrease_allowance(&self, params: ChangeAllowanceParams) -> Result { + /// Decrease the allowance that a spender controls of the owner's balance by the requested delta + /// + /// If the resulting allowance would be negative, the allowance between owner and spender is set + /// to zero. If resulting allowance is zero, the entry is removed from the state map. Returns an + /// error if either the spender or owner address is unresolvable. Returns an error if requested + /// delta is negative. Else returns the new allowance + pub fn decrease_allowance( + &self, + owner: ActorID, + spender: ActorID, + delta: TokenAmount, + ) -> Result { // Load the HAMT holding balances - let state = self.load_state().unwrap(); + let state = self.load_state()?; - let caller_id = self.fvm.caller(); - let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id).unwrap(); + // TODO: replace this with a higher level abstraction where you can call + // state.get_allowance (owner, spender) + let mut allowances_map = state.get_actor_allowance_map(&self.bs, owner)?; - let spender = self.fvm.resolve_address(¶ms.spender)?; - - let new_allowance = match caller_allowances_map.get(&spender)? { + let new_allowance = match allowances_map.get(&spender)? { Some(existing_allowance) => { let new_allowance = existing_allowance .0 - .checked_sub(¶ms.value) + .checked_sub(&delta) .unwrap() // Unwrap should be safe as allowance always > 0 .max(BigInt::zero()); - caller_allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; + allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; new_allowance } None => { // Can't decrease non-existent allowance - return Ok(AllowanceReturn { - owner: params.owner, - spender: params.spender, - value: TokenAmount::zero(), - }); + return Ok(TokenAmount::zero()); } }; - state.save(&self.bs); + state.save(&self.bs)?; - Ok(AllowanceReturn { - owner: params.owner, - spender: params.spender, - value: new_allowance, - }) + Ok(new_allowance) } - pub fn revoke_allowance(&self, params: RevokeAllowanceParams) -> Result { + /// Sets the allowance between owner and spender to 0 + pub fn revoke_allowance(&self, owner: ActorID, spender: ActorID) -> Result<()> { // Load the HAMT holding balances - let state = self.load_state().unwrap(); - - let caller_id = self.fvm.caller(); - let mut caller_allowances_map = state.get_actor_allowance_map(&self.bs, caller_id).unwrap(); - - let spender = self.fvm.resolve_address(¶ms.spender)?; + let state = self.load_state()?; + let mut allowances_map = state.get_actor_allowance_map(&self.bs, owner)?; let new_allowance = TokenAmount::zero(); - caller_allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; + allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; - state.save(&self.bs); - - Ok(AllowanceReturn { - owner: params.owner, - spender: params.spender, - value: new_allowance, - }) + state.save(&self.bs)?; + Ok(()) } - pub fn allowance(&self, params: GetAllowanceParams) -> Result { + /// Gets the allowance between owner and spender + /// + /// The allowance is the amount that the spender can transfer or burn out of the owner's account + /// via the `transfer_from` and `burn_from` methods. + pub fn allowance(&self, owner: ActorID, spender: ActorID) -> Result { // Load the HAMT holding balances - let state = self.load_state().unwrap(); - - let owner = self.fvm.resolve_address(¶ms.owner)?; - let owner_allowances_map = state.get_actor_allowance_map(&self.bs, owner).unwrap(); - - let spender = self.fvm.resolve_address(¶ms.spender)?; + let state = self.load_state()?; + let owner_allowances_map = state.get_actor_allowance_map(&self.bs, owner)?; let allowance = match owner_allowances_map.get(&spender)? { Some(allowance) => allowance.0.clone(), None => TokenAmount::zero(), }; - Ok(AllowanceReturn { - owner: params.owner, - spender: params.spender, - value: allowance, - }) + Ok(allowance) } - pub fn burn(&self, _params: BurnParams) -> Result { - todo!() + /// Burns an amount of token from the specified address, decreasing total token supply + /// + /// ## For all burn operations + /// Preconditions: + /// - The requested value MUST be non-negative + /// - The requested value MUST NOT exceed the target's balance + /// + /// Postconditions: + /// - The target's balance MUST decrease by the requested value + /// - The total_supply MUST decrease by the requested value + /// + /// ## Operator equals target address + /// If the operator is the targeted address, they are implicitly approved to burn an unlimited + /// amount of tokens (up to their balance) + /// + /// ## Operator burning on behalf of target address + /// If the operator is burning on behalf of the target token holder the following preconditions + /// must be met on top of the general burn conditions: + /// - The operator MUST have an allowance not less than the requested value + /// In addition to the general postconditions: + /// - The target-operator allowance MUST decrease by the requested value + /// + /// If the burn operation would result in a negative balance for the targeted address, the burn + /// is discarded and this method returns an error + pub fn burn( + &self, + operator: ActorID, + target: ActorID, + value: TokenAmount, + ) -> Result { + if value.lt(&TokenAmount::zero()) { + bail!("Cannot burn a negative amount"); + } + + let state = self.load_state()?; + + if operator != target { + // attempt to use allowance and return early if not enough + state.attempt_use_allowance(&self.bs, operator, target, &value)?; + } + // attempt to burn the requested amount + let new_amount = state.attempt_burn(&self.bs, target, &value)?; + + state.save(&self.bs)?; + + Ok(new_amount) } pub fn transfer(&self, _params: TransferParams) -> Result { diff --git a/fil_token/src/token/state.rs b/fil_token/src/token/state.rs index f68770c9..b60e2508 100644 --- a/fil_token/src/token/state.rs +++ b/fil_token/src/token/state.rs @@ -13,11 +13,13 @@ use fvm_ipld_hamt::Hamt; use fvm_sdk::sself; use fvm_shared::bigint::bigint_ser; use fvm_shared::bigint::bigint_ser::BigIntDe; +use fvm_shared::bigint::Zero; use fvm_shared::econ::TokenAmount; use fvm_shared::ActorID; -use fvm_shared::HAMT_BIT_WIDTH; -/// Token state ipld structure +const HAMT_BIT_WIDTH: u32 = 5; + +/// Token state IPLD structure #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] pub struct TokenState { /// Total supply of token @@ -26,19 +28,16 @@ pub struct TokenState { /// Map of balances as a Hamt pub balances: Cid, - /// Map> as a Hamt + /// Map> as a Hamt. Allowances are stored balances[owner][spender] pub allowances: Cid, } -/// Functions to get and modify token state to and from the IPLD layer +/// An abstraction over the IPLD layer to get and modify token state without dealing with HAMTs etc. impl TokenState { pub fn new(store: &BS) -> Result { - let empty_balance_map = Hamt::<_, ()>::new_with_bit_width(store, HAMT_BIT_WIDTH) - .flush() - .map_err(|e| anyhow!("Failed to create empty balances map state {}", e))?; - let empty_allowances_map = Hamt::<_, ()>::new_with_bit_width(store, HAMT_BIT_WIDTH) - .flush() - .map_err(|e| anyhow!("Failed to create empty balances map state {}", e))?; + let empty_balance_map = Hamt::<_, ()>::new_with_bit_width(store, HAMT_BIT_WIDTH).flush()?; + let empty_allowances_map = + Hamt::<_, ()>::new_with_bit_width(store, HAMT_BIT_WIDTH).flush()?; Ok(Self { supply: Default::default(), @@ -77,7 +76,25 @@ impl TokenState { Ok(cid) } - pub fn get_balance_map( + /// Get the balance of an ActorID from the currently stored state + pub fn get_balance( + &self, + bs: &BS, + owner: ActorID, + ) -> Result { + let balances = self.get_balance_map(bs)?; + + let balance: TokenAmount; + match balances.get(&owner)? { + Some(amount) => balance = amount.0.clone(), + None => balance = TokenAmount::zero(), + } + + Ok(balance) + } + + /// Retrieve the balance map as a HAMT + fn get_balance_map( &self, bs: &BS, ) -> Result> { @@ -87,6 +104,56 @@ impl TokenState { } } + /// Increase the total supply by the specified value + /// + /// The requested amount must be non-negative. + /// Returns an error if the total supply overflows, else returns the new total supply + pub fn increase_supply(&mut self, value: &TokenAmount) -> Result { + let new_supply = self.supply.checked_add(&value).ok_or_else(|| { + anyhow!( + "Overflow when adding {} to the total_supply of {}", + value, + self.supply + ) + })?; + self.supply = new_supply.clone(); + Ok(new_supply) + } + + /// Attempts to increase the balance of the specified account by the value + /// + /// The requested amount must be non-negative. + /// Returns an error if the balance overflows, else returns the new balance + pub fn increase_balance( + &self, + bs: &BS, + actor: ActorID, + value: &TokenAmount, + ) -> Result { + let mut balance_map = self.get_balance_map(bs)?; + let balance = balance_map.get(&actor)?; + match balance { + Some(existing_amount) => { + let existing_amount = existing_amount.clone().0; + let new_amount = existing_amount.checked_add(&value).ok_or_else(|| { + anyhow!( + "Overflow when adding {} to {}'s balance of {}", + value, + actor, + existing_amount + ) + })?; + + balance_map.set(actor, BigIntDe(new_amount.clone()))?; + Ok(new_amount) + } + None => { + balance_map.set(actor, BigIntDe(value.clone()))?; + Ok(value.clone()) + } + } + } + /// Get the global allowances map /// /// Gets a HAMT with CIDs linking to other HAMTs @@ -127,6 +194,27 @@ impl TokenState { } } + /// TODO: docs + pub fn attempt_burn( + &self, + _bs: BS, + _target: u64, + _value: &TokenAmount, + ) -> Result { + todo!() + } + + /// TODO: docs + pub fn attempt_use_allowance( + &self, + _bs: BS, + _operator: u64, + _target: u64, + _value: &TokenAmount, + ) -> Result { + todo!() + } + // fn enough_allowance( // &self, // bs: &Blockstore, diff --git a/fil_token/src/token/types.rs b/fil_token/src/token/types.rs index 133604fe..7d774ffc 100644 --- a/fil_token/src/token/types.rs +++ b/fil_token/src/token/types.rs @@ -1,3 +1,5 @@ +use anyhow::Result; + use fvm_ipld_encoding::tuple::{Deserialize_tuple, Serialize_tuple}; use fvm_ipld_encoding::{Cbor, RawBytes}; use fvm_shared::address::Address; @@ -5,6 +7,92 @@ use fvm_shared::bigint::bigint_ser; use fvm_shared::econ::TokenAmount; use fvm_shared::ActorID; +/// A standard fungible token interface allowing for on-chain transactions that implements the +/// FRC-XXX standard. This represents the external interface exposed to other on-chain actors +/// +/// Token authors should implement this trait and link the methods to standard dispatch numbers in +/// their actor's `invoke` entrypoint. A standard helper (TODO) is provided to aid method dispatch. +/// +/// TODO: make non-pseudo code +/// ``` +/// //struct Token {} +/// //impl FrcXXXToken for Token { +/// // ... +/// //} +/// +/// //fn invoke(params: u32) -> u32 { +/// // let token = Token {}; +/// +/// // match sdk::message::method_number() { +/// // 1 => constructor(), +/// // 2 => token.name(), +/// // _ => abort!() +/// // // etc. +/// // } +/// //} +/// ``` +pub trait FrcXXXToken { + /// Returns the name of the token + fn name(&self) -> String; + + /// Returns the ticker symbol of the token + fn symbol(&self) -> String; + + /// Returns the total amount of the token in existence + fn total_supply(&self) -> TokenAmount; + + /// Gets the balance of a particular address (if it exists) + /// + /// This will method attempt to resolve addresses to ID-addresses + fn balance_of(&self, params: Address) -> Result; + + /// Atomically increase the amount that a spender can pull from the owner account + /// + /// The increase must be non-negative. Returns the new allowance between those two addresses if + /// successful + fn increase_allowance(&self, params: ChangeAllowanceParams) -> Result; + + /// Atomically decrease the amount that a spender can pull from an account + /// + /// The decrease must be non-negative. The resulting allowance is set to zero if the decrease is + /// more than the current allowance. Returns the new allowance between the two addresses if + /// successful + fn decrease_allowance(&self, params: ChangeAllowanceParams) -> Result; + + /// Set the allowance a spender has on the owner's account to zero + fn revoke_allowance(&self, params: RevokeAllowanceParams) -> Result; + + /// Get the allowance between two addresses + /// + /// The spender can burn or transfer the allowance amount out of the owner's address. If the + /// address of the owner cannot be resolved, this method returns an error. If the owner can be + /// resolved, but the spender address is not registered with an allowance, an implicit allowance + /// of 0 is returned + fn allowance(&self, params: GetAllowanceParams) -> Result; + + /// Burn tokens from the caller's account, decreasing the total supply + /// + /// When burning tokens: + /// - Any holder MUST be allowed to burn their own tokens + /// - The balance of the holder MUST decrease by the amount burned + /// - This method MUST revert if the burn amount is more than the holder's balance + fn burn(&self, params: BurnParams) -> Result; + + /// Burn tokens from the owner's account, decreasing the total supply + /// + /// When burning on behalf of the owner: + /// - The same rules on the holder apply as `burn` + /// - This method MUST revert if the burn amount is more than the allowance authorised by the + /// owner + fn burn_from(&self, params: BurnParams) -> Result; + + /// Transfer tokens from the caller to the receiver + /// + fn transfer(&self, params: TransferParams) -> Result; + + fn transfer_from(&self, params: TransferParams) -> Result; +} + #[derive(Serialize_tuple, Deserialize_tuple)] pub struct MintParams { pub initial_holder: ActorID, diff --git a/fil_token/tests/lib.rs b/fil_token/tests/lib.rs index b6721eb6..14763937 100644 --- a/fil_token/tests/lib.rs +++ b/fil_token/tests/lib.rs @@ -3,9 +3,9 @@ mod runtime; use blockstore::MemoryBlockstore; use runtime::TestRuntime; -use fil_token::token::{Token, TokenHelper}; +use fil_token::token::TokenHelper; #[test] fn it_mints() { - let token = TokenHelper::new(MemoryBlockstore::new(), TestRuntime::new(1)); + // let token = TokenHelper::new(MemoryBlockstore::new(), TestRuntime::new(1)); } diff --git a/fil_token/tests/runtime.rs b/fil_token/tests/runtime.rs index 4aefab98..199f27b2 100644 --- a/fil_token/tests/runtime.rs +++ b/fil_token/tests/runtime.rs @@ -1,5 +1,3 @@ -use std::intrinsics::const_allocate; - use fil_token::runtime::Runtime; pub struct TestRuntime { caller: u64, diff --git a/testing/fil_token_integration/actors/wfil_token_actor/src/lib.rs b/testing/fil_token_integration/actors/wfil_token_actor/src/lib.rs index 65db2888..f78cd330 100644 --- a/testing/fil_token_integration/actors/wfil_token_actor/src/lib.rs +++ b/testing/fil_token_integration/actors/wfil_token_actor/src/lib.rs @@ -1,8 +1,7 @@ use anyhow::Result; use fil_token::blockstore::Blockstore; use fil_token::runtime::FvmRuntime; -use fil_token::token::types::*; -use fil_token::token::{Token, TokenHelper}; +use fil_token::token::*; use fvm_ipld_encoding::{RawBytes, DAG_CBOR}; use fvm_sdk as sdk; use fvm_shared::bigint::bigint_ser::BigIntSer; @@ -14,9 +13,9 @@ struct WfilToken { util: TokenHelper, } -// TODO: Wrapper is unecessary? -// Instead expose a -impl Token for WfilToken { +/// Implement the token API +/// here addresses should be translated to actor id's etc. +impl FrcXXXToken for WfilToken { fn name(&self) -> String { String::from("Wrapped FIL") } @@ -33,31 +32,40 @@ impl Token for WfilToken { &self, params: fvm_shared::address::Address, ) -> Result { - self.util.balance_of(params) + todo!("reshape aprams") } fn increase_allowance(&self, params: ChangeAllowanceParams) -> Result { - self.util.increase_allowance(params) + todo!("resolve address to actorid"); } fn decrease_allowance(&self, params: ChangeAllowanceParams) -> Result { - self.util.decrease_allowance(params) + todo!("add return") } fn revoke_allowance(&self, params: RevokeAllowanceParams) -> Result { - self.util.revoke_allowance(params) + todo!("add return") } fn allowance(&self, params: GetAllowanceParams) -> Result { - self.util.allowance(params) + todo!(); } + // TODO: change burn params fn burn(&self, params: BurnParams) -> Result { - self.util.burn(params) + todo!(); } fn transfer(&self, params: TransferParams) -> Result { - self.util.transfer(params) + todo!() + } + + fn burn_from(&self, params: BurnParams) -> Result { + todo!(); + } + + fn transfer_from(&self, params: TransferParams) -> Result { + todo!() } } @@ -67,8 +75,9 @@ pub fn invoke(params: u32) -> u32 { // Conduct method dispatch. Handle input parameters and return data. let method_num = sdk::message::method_number(); + let root_cid = sdk::sself::root().unwrap(); let token_actor = WfilToken { - util: TokenHelper::new(Blockstore {}, FvmRuntime {}), + util: TokenHelper::new(Blockstore {}, FvmRuntime {}, root_cid), }; //TODO: this internal dispatch can be pushed as a library function into the fil_token crate @@ -129,12 +138,10 @@ pub fn invoke(params: u32) -> u32 { } // Custom actor interface 12 => { - // Mint - let params = MintParams { - initial_holder: sdk::message::caller(), - value: sdk::message::value_received(), - }; - let res = token_actor.util.mint(params).unwrap(); + let res = token_actor + .util + .mint(sdk::message::caller(), sdk::message::value_received()) + .unwrap(); let res = RawBytes::new(fvm_ipld_encoding::to_vec(&res).unwrap()); let cid = sdk::ipld::put_block(DAG_CBOR, res.bytes()).unwrap(); cid From 2008cbd2fc99c4a72bc9a751bcf3cdfc903dd114 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Fri, 15 Jul 2022 13:25:39 +1000 Subject: [PATCH 17/21] refactor allowance code --- fil_token/src/token/mod.rs | 94 ++------ fil_token/src/token/state.rs | 261 +++++++++++++++++---- testing/fil_token_integration/tests/lib.rs | 1 - 3 files changed, 233 insertions(+), 123 deletions(-) diff --git a/fil_token/src/token/mod.rs b/fil_token/src/token/mod.rs index 51ae07c4..3b1fdfce 100644 --- a/fil_token/src/token/mod.rs +++ b/fil_token/src/token/mod.rs @@ -1,8 +1,7 @@ pub mod errors; pub mod receiver; -pub mod state; +mod state; mod types; -use self::errors::ActorError; use self::state::TokenState; pub use self::types::*; use crate::runtime::Runtime; @@ -12,8 +11,6 @@ use anyhow::Ok; use anyhow::Result; use cid::Cid; use fvm_ipld_blockstore::Blockstore as IpldStore; -use fvm_shared::bigint::bigint_ser::BigIntDe; -use fvm_shared::bigint::BigInt; use fvm_shared::bigint::Zero; use fvm_shared::econ::TokenAmount; use fvm_shared::ActorID; @@ -97,6 +94,16 @@ where state.get_balance(&self.bs, holder) } + /// Gets the allowance between owner and spender + /// + /// The allowance is the amount that the spender can transfer or burn out of the owner's account + /// via the `transfer_from` and `burn_from` methods. + pub fn allowance(&self, owner: ActorID, spender: ActorID) -> Result { + let state = self.load_state()?; + let allowance = state.get_allowance_between(&self.bs, owner, spender)?; + Ok(allowance) + } + /// Increase the allowance that a spender controls of the owner's balance by the requested delta /// /// Returns an error if requested delta is negative or there are errors in (de)sereliazation of @@ -108,32 +115,11 @@ where delta: TokenAmount, ) -> Result { if delta.lt(&TokenAmount::zero()) { - bail!("value of allowance increase was negative {}", delta); + bail!("value of delta was negative {}", delta); } - // Retrieve the HAMT holding balances - let state = self.load_state()?; - let mut owner_allowances = state.get_actor_allowance_map(&self.bs, owner)?; - - let new_amount = match owner_allowances.get(&spender)? { - // Allowance exists - attempt to calculate new allowance - Some(existing_allowance) => match existing_allowance.0.checked_add(&delta) { - Some(new_allowance) => { - owner_allowances.set(spender, BigIntDe(new_allowance.clone()))?; - new_allowance - } - None => bail!(ActorError::Arithmetic(format!( - "allowance overflowed attempting to add {} to existing allowance of {} between {} {}", - delta, existing_allowance.0, owner, spender - ))), - }, - // No allowance recorded previously - None => { - owner_allowances.set(spender, BigIntDe(delta.clone()))?; - delta - } - }; - + let mut state = self.load_state()?; + let new_amount = state.increase_allowance(&self.bs, owner, spender, &delta)?; state.save(&self.bs)?; Ok(new_amount) @@ -151,29 +137,12 @@ where spender: ActorID, delta: TokenAmount, ) -> Result { - // Load the HAMT holding balances - let state = self.load_state()?; - - // TODO: replace this with a higher level abstraction where you can call - // state.get_allowance (owner, spender) - let mut allowances_map = state.get_actor_allowance_map(&self.bs, owner)?; - - let new_allowance = match allowances_map.get(&spender)? { - Some(existing_allowance) => { - let new_allowance = existing_allowance - .0 - .checked_sub(&delta) - .unwrap() // Unwrap should be safe as allowance always > 0 - .max(BigInt::zero()); - allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; - new_allowance - } - None => { - // Can't decrease non-existent allowance - return Ok(TokenAmount::zero()); - } - }; + if delta.lt(&TokenAmount::zero()) { + bail!("value of delta was negative {}", delta); + } + let mut state = self.load_state()?; + let new_allowance = state.decrease_allowance(&self.bs, owner, spender, &delta)?; state.save(&self.bs)?; Ok(new_allowance) @@ -181,33 +150,12 @@ where /// Sets the allowance between owner and spender to 0 pub fn revoke_allowance(&self, owner: ActorID, spender: ActorID) -> Result<()> { - // Load the HAMT holding balances let state = self.load_state()?; - let mut allowances_map = state.get_actor_allowance_map(&self.bs, owner)?; - let new_allowance = TokenAmount::zero(); - allowances_map.set(spender, BigIntDe(new_allowance.clone()))?; - + state.revoke_allowance(&self.bs, owner, spender)?; state.save(&self.bs)?; Ok(()) } - /// Gets the allowance between owner and spender - /// - /// The allowance is the amount that the spender can transfer or burn out of the owner's account - /// via the `transfer_from` and `burn_from` methods. - pub fn allowance(&self, owner: ActorID, spender: ActorID) -> Result { - // Load the HAMT holding balances - let state = self.load_state()?; - - let owner_allowances_map = state.get_actor_allowance_map(&self.bs, owner)?; - let allowance = match owner_allowances_map.get(&spender)? { - Some(allowance) => allowance.0.clone(), - None => TokenAmount::zero(), - }; - - Ok(allowance) - } - /// Burns an amount of token from the specified address, decreasing total token supply /// /// ## For all burn operations @@ -251,8 +199,8 @@ where // attempt to burn the requested amount let new_amount = state.attempt_burn(&self.bs, target, &value)?; + // if both succeeded, atomically commit the transaction state.save(&self.bs)?; - Ok(new_amount) } diff --git a/fil_token/src/token/state.rs b/fil_token/src/token/state.rs index b60e2508..0abeacb8 100644 --- a/fil_token/src/token/state.rs +++ b/fil_token/src/token/state.rs @@ -34,6 +34,7 @@ pub struct TokenState { /// An abstraction over the IPLD layer to get and modify token state without dealing with HAMTs etc. impl TokenState { + /// Create a new token state-tree, without committing it to a blockstore pub fn new(store: &BS) -> Result { let empty_balance_map = Hamt::<_, ()>::new_with_bit_width(store, HAMT_BIT_WIDTH).flush()?; let empty_allowances_map = @@ -93,6 +94,40 @@ impl TokenState { Ok(balance) } + /// Attempts to increase the balance of the specified account by the value + /// + /// The requested amount must be non-negative. + /// Returns an error if the balance overflows, else returns the new balance + pub fn increase_balance( + &self, + bs: &BS, + actor: ActorID, + value: &TokenAmount, + ) -> Result { + let mut balance_map = self.get_balance_map(bs)?; + let balance = balance_map.get(&actor)?; + match balance { + Some(existing_amount) => { + let existing_amount = existing_amount.clone().0; + let new_amount = existing_amount.checked_add(&value).ok_or_else(|| { + anyhow!( + "Overflow when adding {} to {}'s balance of {}", + value, + actor, + existing_amount + ) + })?; + + balance_map.set(actor, BigIntDe(new_amount.clone()))?; + Ok(new_amount) + } + None => { + balance_map.set(actor, BigIntDe(value.clone()))?; + Ok(value.clone()) + } + } + } + /// Retrieve the balance map as a HAMT fn get_balance_map( &self, @@ -120,78 +155,206 @@ impl TokenState { Ok(new_supply) } - /// Attempts to increase the balance of the specified account by the value + /// Get the allowance that an owner has approved for a spender /// - /// The requested amount must be non-negative. - /// Returns an error if the balance overflows, else returns the new balance - pub fn increase_balance( + /// If an existing allowance cannot be found, it is implicitly assumed to be zero + pub fn get_allowance_between( &self, bs: &BS, - actor: ActorID, + owner: ActorID, + spender: ActorID, + ) -> Result { + let owner_allowances = self.get_owner_allowance_map(bs, owner)?; + match owner_allowances { + Some(hamt) => { + let maybe_allowance = hamt.get(&spender)?; + if let Some(allowance) = maybe_allowance { + return Ok(allowance.clone().0); + } + Ok(TokenAmount::zero()) + } + None => Ok(TokenAmount::zero()), + } + } + + /// Increase the allowance between owner and spender by the specified value + /// + /// Caller must ensure that value is non-negative. + pub fn increase_allowance( + &mut self, + bs: &BS, + owner: ActorID, + spender: ActorID, value: &TokenAmount, ) -> Result { - let mut balance_map = self.get_balance_map(bs)?; - let balance = balance_map.get(&actor)?; - match balance { - Some(existing_amount) => { - let existing_amount = existing_amount.clone().0; - let new_amount = existing_amount.checked_add(&value).ok_or_else(|| { + if value.is_zero() { + // This is a no-op as far as mutating state + return self.get_allowance_between(bs, owner, spender); + } + + let allowance_map = self.get_owner_allowance_map(bs, owner)?; + + // If allowance map exists, modify or insert the allowance + if let Some(mut hamt) = allowance_map { + let previous_allowance = hamt.get(&spender)?; + + // Calculate the new allowance + let new_allowance = match previous_allowance { + Some(prev_allowance) => prev_allowance.0.checked_add(&value).ok_or_else(|| { anyhow!( - "Overflow when adding {} to {}'s balance of {}", + "Overflow when adding {} to {}'s allowance of {}", value, - actor, - existing_amount + spender, + prev_allowance.0 ) - })?; + })?, + None => value.clone(), + }; + + // TODO: should this be set as a BigIntSer rather than BigIntDe? + hamt.set(spender, BigIntDe(new_allowance.clone()))?; + + { + // TODO: helper functions for saving hamts?, this can probably be done more efficiently rather than + // getting the root allowance map again, by abstracting the nested hamt structure + let new_cid = hamt.flush()?; + let mut root_allowance_map = self.get_allowances_map(bs)?; + root_allowance_map.set(owner, new_cid)?; + let new_cid = root_allowance_map.flush(); + self.allowances = new_cid?; + } - balance_map.set(actor, BigIntDe(new_amount.clone()))?; - Ok(new_amount) + return Ok(new_allowance); + } + + // If allowance map does not exist, create it and insert the allowance + let mut owner_allowances = + Hamt::::new_with_bit_width(*bs, HAMT_BIT_WIDTH); + owner_allowances.set(spender, BigIntDe(value.clone()))?; + + { + // TODO: helper functions for saving hamts?, this can probably be done more efficiently rather than + // getting the root allowance map again, by abstracting the nested hamt structure + let mut root_allowance_map = self.get_allowances_map(bs)?; + root_allowance_map.set(owner, owner_allowances.flush()?)?; + self.allowances = root_allowance_map.flush()?; + } + + return Ok((*value).clone()); + } + + /// Decrease the allowance between owner and spender by the specified value. If the resulting + /// allowance is negative, it is set to zero. + /// + /// Caller must ensure that value is non-negative. + /// + /// If the allowance is decreased to zero, the entry is removed from the map. + /// If the map is empty, it is removed from the root map. + pub fn decrease_allowance( + &mut self, + bs: &BS, + owner: ActorID, + spender: ActorID, + value: &TokenAmount, + ) -> Result { + if value.is_zero() { + // This is a no-op as far as mutating state + return self.get_allowance_between(bs, owner, spender); + } + + let allowance_map = self.get_owner_allowance_map(bs, owner)?; + + // If allowance map exists, modify or insert the allowance + if let Some(mut hamt) = allowance_map { + let previous_allowance = hamt.get(&spender)?; + + // Calculate the new allowance, and max with zero + let new_allowance = match previous_allowance { + Some(prev_allowance) => prev_allowance.0.checked_sub(&value).ok_or_else(|| { + anyhow!( + "Overflow when adding {} to {}'s allowance of {}", + value, + spender, + prev_allowance.0 + ) + })?, + None => value.clone(), } - None => { - balance_map.set(actor, BigIntDe(value.clone()))?; - Ok(value.clone()) + .max(TokenAmount::zero()); + + // Update the Hamts + let mut root_allowance_map = self.get_allowances_map(bs)?; + + if new_allowance.is_zero() { + hamt.delete(&spender)?; + + if hamt.is_empty() { + root_allowance_map.delete(&owner)?; + } else { + root_allowance_map.set(owner, hamt.flush()?)?; + } + + self.allowances = root_allowance_map.flush()?; + return Ok(TokenAmount::zero()); } + + // TODO: should this be set as a BigIntSer rather than BigIntDe? + hamt.set(spender, BigIntDe(new_allowance.clone()))?; + { + // TODO: helper functions for saving hamts?, this can probably be done more efficiently rather than + // getting the root allowance map again, by abstracting the nested hamt structure + root_allowance_map.set(owner, hamt.flush()?)?; + self.allowances = root_allowance_map.flush()?; + } + + return Ok(new_allowance); } + + // If allowance map does not exist, decreasing is a no-op + Ok(TokenAmount::zero()) } - /// Get the global allowances map + /// Revokes an approved allowance by removing the entry from the owner-spender map /// - /// Gets a HAMT with CIDs linking to other HAMTs - pub fn get_allowances_map( + /// If that map becomes empty, it is removed from the root map. + pub fn revoke_allowance( &self, bs: &BS, - ) -> Result> { - match Hamt::::load(&self.allowances, *bs) { - Ok(map) => Ok(map), - Err(err) => return Err(anyhow!("Failed to load allowances hamt: {:?}", err)), + owner: ActorID, + spender: ActorID, + ) -> Result<()> { + let allowance_map = self.get_owner_allowance_map(bs, owner)?; + if let Some(mut map) = allowance_map { + map.delete(&spender)?; } + + Ok(()) } - /// Get the allowances map of a specific actor, lazily creating one if it didn't exist - /// TODO: don't lazily create this, higher logic needed to get allowances etc. - pub fn get_actor_allowance_map( + /// Get the allowances map of a specific actor, resolving the CID link to a Hamt + /// + /// Ok(Some) if the owner has allocated allowances to other actors + /// Ok(None) if the owner has no current non-zero allowances to other actors + /// Err if operations on the underlying Hamt failed + fn get_owner_allowance_map( &self, bs: &BS, owner: ActorID, - ) -> Result> { - let mut global_allowances = self.get_allowances_map(bs).unwrap(); - match global_allowances.get(&owner) { - Ok(Some(map)) => { - // authorising actor already had an allowance map, return it - Ok(Hamt::::load(map, *bs).unwrap()) - } - Ok(None) => { - // authorising actor does not have an allowance map, create one and return it - let mut new_actor_allowances = Hamt::new(*bs); - let cid = new_actor_allowances - .flush() - .map_err(|e| anyhow!("Failed to create empty balances map state {}", e)) - .unwrap(); - global_allowances.set(owner, cid).unwrap(); - Ok(new_actor_allowances) - } - Err(e) => Err(anyhow!("failed to get actor's allowance map {:?}", e)), - } + ) -> Result>> { + let allowances_map = self.get_allowances_map(bs)?; + let owner_allowances = match allowances_map.get(&owner)? { + Some(cid) => Some(Hamt::::load(cid, *bs)?), + None => None, + }; + Ok(owner_allowances) + } + + /// Get the global allowances map + /// + /// Gets a HAMT with CIDs linking to other HAMTs + fn get_allowances_map(&self, bs: &BS) -> Result> { + Hamt::::load(&self.allowances, *bs) + .map_err(|e| anyhow!("Failed to load base allowances map {}", e)) } /// TODO: docs diff --git a/testing/fil_token_integration/tests/lib.rs b/testing/fil_token_integration/tests/lib.rs index 806ac09f..2cb23d44 100644 --- a/testing/fil_token_integration/tests/lib.rs +++ b/testing/fil_token_integration/tests/lib.rs @@ -1,7 +1,6 @@ use std::env; use fil_token::blockstore::Blockstore as ActorBlockstore; -use fil_token::token::state::TokenState; use fvm_integration_tests::tester::{Account, Tester}; use fvm_ipld_blockstore::MemoryBlockstore; use fvm_shared::address::Address; From 400f2f150ecb8540aa891ffd3ddd626026a27182 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Sun, 17 Jul 2022 01:01:23 +1000 Subject: [PATCH 18/21] finish core implementation of frc-xxx token --- fil_token/src/token/mod.rs | 74 ++++++++-- fil_token/src/token/state.rs | 252 ++++++++++++++++++----------------- 2 files changed, 190 insertions(+), 136 deletions(-) diff --git a/fil_token/src/token/mod.rs b/fil_token/src/token/mod.rs index 3b1fdfce..e4067c06 100644 --- a/fil_token/src/token/mod.rs +++ b/fil_token/src/token/mod.rs @@ -150,7 +150,7 @@ where /// Sets the allowance between owner and spender to 0 pub fn revoke_allowance(&self, owner: ActorID, spender: ActorID) -> Result<()> { - let state = self.load_state()?; + let mut state = self.load_state()?; state.revoke_allowance(&self.bs, owner, spender)?; state.save(&self.bs)?; Ok(()) @@ -159,11 +159,10 @@ where /// Burns an amount of token from the specified address, decreasing total token supply /// /// ## For all burn operations - /// Preconditions: /// - The requested value MUST be non-negative /// - The requested value MUST NOT exceed the target's balance /// - /// Postconditions: + /// Upon successful burn /// - The target's balance MUST decrease by the requested value /// - The total_supply MUST decrease by the requested value /// @@ -182,29 +181,82 @@ where /// is discarded and this method returns an error pub fn burn( &self, - operator: ActorID, - target: ActorID, + spender: ActorID, + owner: ActorID, value: TokenAmount, ) -> Result { if value.lt(&TokenAmount::zero()) { bail!("Cannot burn a negative amount"); } - let state = self.load_state()?; + let mut state = self.load_state()?; - if operator != target { + if spender != owner { // attempt to use allowance and return early if not enough - state.attempt_use_allowance(&self.bs, operator, target, &value)?; + state.attempt_use_allowance(&self.bs, spender, owner, &value)?; } // attempt to burn the requested amount - let new_amount = state.attempt_burn(&self.bs, target, &value)?; + let new_amount = state.decrease_balance(&self.bs, owner, &value)?; // if both succeeded, atomically commit the transaction state.save(&self.bs)?; Ok(new_amount) } - pub fn transfer(&self, _params: TransferParams) -> Result { - todo!() + /// Transfers an amount from one actor to another + /// + /// ## For all transfer operations + /// + /// - The requested value MUST be non-negative + /// - The requested value MUST NOT exceed the sender's balance + /// - The receiver actor MUST implement a method called `tokens_received`, corresponding to the + /// interface specified for FRC-XXX token receivers + /// - The receiver's `tokens_received` hook MUST NOT abort + /// + /// Upon successful transfer: + /// - The senders's balance MUST decrease by the requested value + /// - The receiver's balance MUST increase by the requested value + /// + /// ## Operator equals target address + /// If the operator is the 'from' address, they are implicitly approved to transfer an unlimited + /// amount of tokens (up to their balance) + /// + /// ## Operator transferring on behalf of target address + /// If the operator is transferring on behalf of the target token holder the following preconditions + /// must be met on top of the general burn conditions: + /// - The operator MUST have an allowance not less than the requested value + /// In addition to the general postconditions: + /// - The from-operator allowance MUST decrease by the requested value + pub fn transfer( + &self, + operator: ActorID, + from: ActorID, + to: ActorID, + value: TokenAmount, + ) -> Result<()> { + if value.lt(&TokenAmount::zero()) { + bail!("Cannot transfer a negative amount"); + } + + let mut state = self.load_state()?; + + if operator != from { + // attempt to use allowance and return early if not enough + state.attempt_use_allowance(&self.bs, operator, from, &value)?; + } + + // call the receiver hook + // FIXME: use fvm_dispatch to make a standard runtime call to the receiver + // - ensure the hook did not abort + + // attempt to debit from the sender + state.decrease_balance(&self.bs, from, &value)?; + // attempt to credit the receiver + state.increase_balance(&self.bs, to, &value)?; + + // if all succeeded, atomically commit the transaction + state.save(&self.bs)?; + + Ok(()) } } diff --git a/fil_token/src/token/state.rs b/fil_token/src/token/state.rs index 0abeacb8..e9fa21b6 100644 --- a/fil_token/src/token/state.rs +++ b/fil_token/src/token/state.rs @@ -1,4 +1,5 @@ use anyhow::anyhow; +use anyhow::bail; use anyhow::Result; use cid::multihash::Code; use cid::Cid; @@ -10,7 +11,6 @@ use fvm_ipld_encoding::Cbor; use fvm_ipld_encoding::CborStore; use fvm_ipld_encoding::DAG_CBOR; use fvm_ipld_hamt::Hamt; -use fvm_sdk::sself; use fvm_shared::bigint::bigint_ser; use fvm_shared::bigint::bigint_ser::BigIntDe; use fvm_shared::bigint::Zero; @@ -33,6 +33,12 @@ pub struct TokenState { } /// An abstraction over the IPLD layer to get and modify token state without dealing with HAMTs etc. +/// +/// This is a simple wrapper of state and in general does not account for token protocol level +/// checks such as ensuring necessary approvals are enforced during transfers. This is left for the +/// caller to handle. However, some invariants such as enforcing non-negative balances, allowances +/// and total supply are enforced. Furthermore, this layer returns errors if any of the underlying +/// arithmetic overflows. impl TokenState { /// Create a new token state-tree, without committing it to a blockstore pub fn new(store: &BS) -> Result { @@ -57,7 +63,7 @@ impl TokenState { } } - /// Saves the current state to the blockstore + /// Saves the current state to the blockstore, returning the cid pub fn save(&self, bs: &BS) -> Result { let serialized = match fvm_ipld_encoding::to_vec(self) { Ok(s) => s, @@ -71,9 +77,6 @@ impl TokenState { Ok(cid) => cid, Err(err) => return Err(anyhow!("failed to store initial state: {:}", err)), }; - if let Err(err) = sself::set_root(&cid) { - return Err(anyhow!("failed to set root ciid: {:}", err)); - } Ok(cid) } @@ -96,17 +99,19 @@ impl TokenState { /// Attempts to increase the balance of the specified account by the value /// - /// The requested amount must be non-negative. + /// Caller must ensure the requested amount is non-negative. /// Returns an error if the balance overflows, else returns the new balance pub fn increase_balance( - &self, + &mut self, bs: &BS, actor: ActorID, value: &TokenAmount, ) -> Result { let mut balance_map = self.get_balance_map(bs)?; let balance = balance_map.get(&actor)?; - match balance { + + // calculate the new balance + let new_balance = match balance { Some(existing_amount) => { let existing_amount = existing_amount.clone().0; let new_amount = existing_amount.checked_add(&value).ok_or_else(|| { @@ -118,14 +123,67 @@ impl TokenState { ) })?; - balance_map.set(actor, BigIntDe(new_amount.clone()))?; - Ok(new_amount) + new_amount } None => { balance_map.set(actor, BigIntDe(value.clone()))?; - Ok(value.clone()) + self.balances = balance_map.flush()?; + value.clone() } + }; + + // update the state with the new balance + balance_map.set(actor, BigIntDe(new_balance.clone()))?; + self.balances = balance_map.flush()?; + Ok(new_balance) + } + + /// Attempts to decrease the balance of the specified account by the value + /// + /// Caller must ensure the requested amount is non-negative. + /// Returns an error if the balance overflows, or if resulting balance would be negative. + /// Else returns the new balance + pub fn decrease_balance( + &mut self, + bs: &BS, + actor: ActorID, + value: &TokenAmount, + ) -> Result { + let mut balance_map = self.get_balance_map(bs)?; + let balance = balance_map.get(&actor)?; + + if balance.is_none() { + bail!( + "Balance would be negative after subtracting {} from {}'s balance of {}", + value, + actor, + TokenAmount::zero() + ); + } + + let existing_amount = balance.unwrap().clone().0; + let new_amount = existing_amount.checked_sub(&value).ok_or_else(|| { + anyhow!( + "Overflow when subtracting {} from {}'s balance of {}", + value, + actor, + existing_amount + ) + })?; + + if new_amount.lt(&TokenAmount::zero()) { + bail!( + "Balance would be negative after subtracting {} from {}'s balance of {}", + value, + actor, + existing_amount + ); } + + balance_map.set(actor, BigIntDe(new_amount.clone()))?; + self.balances = balance_map.flush()?; + + Ok(new_amount) } /// Retrieve the balance map as a HAMT @@ -318,7 +376,7 @@ impl TokenState { /// /// If that map becomes empty, it is removed from the root map. pub fn revoke_allowance( - &self, + &mut self, bs: &BS, owner: ActorID, spender: ActorID, @@ -326,11 +384,68 @@ impl TokenState { let allowance_map = self.get_owner_allowance_map(bs, owner)?; if let Some(mut map) = allowance_map { map.delete(&spender)?; + if map.is_empty() { + let mut root_allowance_map = self.get_allowances_map(bs)?; + root_allowance_map.delete(&owner)?; + self.allowances = root_allowance_map.flush()?; + } else { + let new_cid = map.flush()?; + let mut root_allowance_map = self.get_allowances_map(bs)?; + root_allowance_map.set(owner, new_cid)?; + self.allowances = root_allowance_map.flush()?; + } } Ok(()) } + /// Atomically checks if value is less than the allowance and deducts it if so + /// + /// Returns new allowance if successful, else returns an error and the allowance is unchanged + pub fn attempt_use_allowance( + &mut self, + bs: &BS, + spender: u64, + owner: u64, + value: &TokenAmount, + ) -> Result { + let current_allowance = self.get_allowance_between(bs, owner, spender)?; + + if value.is_zero() { + return Ok(current_allowance); + } + + let new_allowance = current_allowance.checked_sub(value).ok_or_else(|| { + anyhow!( + "Overflow when subtracting {} from {}'s allowance of {}", + value, + owner, + current_allowance + ) + })?; + + if new_allowance.lt(&TokenAmount::zero()) { + return Err(anyhow!( + "Attempted to use {} of {}'s tokens from {}'s allowance of {}", + value, + owner, + spender, + current_allowance + )); + } + + // TODO: helper function to set a new allowance and flush hamts + let owner_allowances = self.get_owner_allowance_map(bs, owner)?; + // to reach here, allowance must have been previously non zero; so safe to assume the map exists + let mut owner_allowances = owner_allowances.unwrap(); + owner_allowances.set(spender, BigIntDe(new_allowance.clone()))?; + let mut allowance_map = self.get_allowances_map(bs)?; + allowance_map.set(owner, owner_allowances.flush()?)?; + self.allowances = allowance_map.flush()?; + + Ok(new_allowance) + } + /// Get the allowances map of a specific actor, resolving the CID link to a Hamt /// /// Ok(Some) if the owner has allocated allowances to other actors @@ -356,119 +471,6 @@ impl TokenState { Hamt::::load(&self.allowances, *bs) .map_err(|e| anyhow!("Failed to load base allowances map {}", e)) } - - /// TODO: docs - pub fn attempt_burn( - &self, - _bs: BS, - _target: u64, - _value: &TokenAmount, - ) -> Result { - todo!() - } - - /// TODO: docs - pub fn attempt_use_allowance( - &self, - _bs: BS, - _operator: u64, - _target: u64, - _value: &TokenAmount, - ) -> Result { - todo!() - } - - // fn enough_allowance( - // &self, - // bs: &Blockstore, - // from: ActorID, - // spender: ActorID, - // to: ActorID, - // amount: &TokenAmount, - // ) -> std::result::Result<(), TokenAmountDiff> { - // if spender == from { - // return std::result::Result::Ok(()); - // } - - // let allowances = self.get_actor_allowance_map(bs, from); - // let allowance = match allowances.get(&to) { - // Ok(Some(amount)) => amount.0.clone(), - // _ => TokenAmount::zero(), - // }; - - // if allowance.lt(&amount) { - // Err(TokenAmountDiff { - // actual: allowance, - // required: amount.clone(), - // }) - // } else { - // std::result::Result::Ok(()) - // } - // } - - // fn enough_balance( - // &self, - // bs: &Blockstore, - // from: ActorID, - // amount: &TokenAmount, - // ) -> std::result::Result<(), TokenAmountDiff> { - // let balances = self.get_balance_map(bs); - // let balance = match balances.get(&from) { - // Ok(Some(amount)) => amount.0.clone(), - // _ => TokenAmount::zero(), - // }; - - // if balance.lt(&amount) { - // Err(TokenAmountDiff { - // actual: balance, - // required: amount.clone(), - // }) - // } else { - // std::result::Result::Ok(()) - // } - // } - - // /// Atomically make a transfer - // fn make_transfer( - // &self, - // bs: &Blockstore, - // amount: &TokenAmount, - // from: ActorID, - // spender: ActorID, - // to: ActorID, - // ) -> TransferResult { - // if let Err(e) = self.enough_allowance(bs, from, spender, to, amount) { - // return Err(TransferError::InsufficientAllowance(e)); - // } - // if let Err(e) = self.enough_balance(bs, from, amount) { - // return Err(TransferError::InsufficientBalance(e)); - // } - - // // Decrease allowance, decrease balance - // // From the above checks, we know these exist - // // TODO: do this in a transaction to avoid re-entrancy bugs - // let mut allowances = self.get_actor_allowance_map(bs, from); - // let allowance = allowances.get(&to).unwrap().unwrap(); - // let new_allowance = allowance.0.clone().sub(amount); - // allowances.set(to, BigIntDe(new_allowance)).unwrap(); - - // let mut balances = self.get_balance_map(bs); - // let sender_balance = balances.get(&from).unwrap().unwrap(); - // let new_sender_balance = sender_balance.0.clone().sub(amount); - // balances.set(from, BigIntDe(new_sender_balance)).unwrap(); - - // // TODO: call the receive hook - - // // TODO: if no hook, revert the balance and allowance change - - // // if successful, mark the balance as having been credited - - // let receiver_balance = balances.get(&to).unwrap().unwrap(); - // let new_receiver_balance = receiver_balance.0.clone().add(amount); - // balances.set(to, BigIntDe(new_receiver_balance)).unwrap(); - - // Ok(amount.clone()) - // } } impl Cbor for TokenState {} From 258e2ae1c2802dbc1913eab900a92e1bc1966eb0 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Sun, 17 Jul 2022 02:18:39 +1000 Subject: [PATCH 19/21] failing tests --- fil_token/src/blockstore.rs | 29 ++++++++++ fil_token/src/token/mod.rs | 21 +++----- fil_token/src/token/state.rs | 99 +++++++++++++++++++++++++++-------- fil_token/tests/blockstore.rs | 32 ----------- fil_token/tests/lib.rs | 5 +- 5 files changed, 112 insertions(+), 74 deletions(-) diff --git a/fil_token/src/blockstore.rs b/fil_token/src/blockstore.rs index c579b914..c492af22 100644 --- a/fil_token/src/blockstore.rs +++ b/fil_token/src/blockstore.rs @@ -41,3 +41,32 @@ impl fvm_ipld_blockstore::Blockstore for Blockstore { Ok(k) } } + +// TODO: put this somewhere more appropriate when a tests folder exists +/// An in-memory blockstore impl taken from filecoin-project/ref-fvm +#[derive(Debug, Default, Clone)] +pub struct MemoryBlockstore { + blocks: RefCell>>, +} + +use std::{cell::RefCell, collections::HashMap}; +impl MemoryBlockstore { + pub fn new() -> Self { + Self::default() + } +} + +impl fvm_ipld_blockstore::Blockstore for MemoryBlockstore { + fn has(&self, k: &Cid) -> Result { + Ok(self.blocks.borrow().contains_key(k)) + } + + fn get(&self, k: &Cid) -> Result>> { + Ok(self.blocks.borrow().get(k).cloned()) + } + + fn put_keyed(&self, k: &Cid, block: &[u8]) -> Result<()> { + self.blocks.borrow_mut().insert(*k, block.into()); + Ok(()) + } +} diff --git a/fil_token/src/token/mod.rs b/fil_token/src/token/mod.rs index e4067c06..22e8ed69 100644 --- a/fil_token/src/token/mod.rs +++ b/fil_token/src/token/mod.rs @@ -4,7 +4,6 @@ mod state; mod types; use self::state::TokenState; pub use self::types::*; -use crate::runtime::Runtime; use anyhow::bail; use anyhow::Ok; @@ -18,31 +17,23 @@ use fvm_shared::ActorID; /// Library functions that implement core FRC-??? standards /// /// Holds injectable services to access/interface with IPLD/FVM layer. -pub struct TokenHelper +pub struct TokenHelper where - BS: IpldStore + Copy, - FVM: Runtime, + BS: IpldStore + Clone, { /// Injected blockstore bs: BS, - /// Access to the runtime - _runtime: FVM, /// Root of the token state tree token_state: Cid, } -impl TokenHelper +impl TokenHelper where - BS: IpldStore + Copy, - FVM: Runtime, + BS: IpldStore + Clone, { /// Instantiate a token helper with access to a blockstore and runtime - pub fn new(bs: BS, runtime: FVM, token_state: Cid) -> Self { - Self { - bs, - _runtime: runtime, - token_state, - } + pub fn new(bs: BS, token_state: Cid) -> Self { + Self { bs, token_state } } /// Constructs the token state tree and saves it at a CID diff --git a/fil_token/src/token/state.rs b/fil_token/src/token/state.rs index e9fa21b6..de1fb7b1 100644 --- a/fil_token/src/token/state.rs +++ b/fil_token/src/token/state.rs @@ -17,10 +17,12 @@ use fvm_shared::bigint::Zero; use fvm_shared::econ::TokenAmount; use fvm_shared::ActorID; +use crate::blockstore::Blockstore; + const HAMT_BIT_WIDTH: u32 = 5; /// Token state IPLD structure -#[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] +#[derive(Serialize_tuple, Deserialize_tuple, PartialEq, Clone, Debug)] pub struct TokenState { /// Total supply of token #[serde(with = "bigint_ser")] @@ -39,6 +41,10 @@ pub struct TokenState { /// caller to handle. However, some invariants such as enforcing non-negative balances, allowances /// and total supply are enforced. Furthermore, this layer returns errors if any of the underlying /// arithmetic overflows. +/// +/// Some methods on TokenState require the caller to pass in a blockstore implementing the Clone +/// trait. It is assumed that when cloning the blockstore implementation does a "shallow-clone" +/// of the blockstore and provides access to the same underlying data. impl TokenState { /// Create a new token state-tree, without committing it to a blockstore pub fn new(store: &BS) -> Result { @@ -64,7 +70,12 @@ impl TokenState { } /// Saves the current state to the blockstore, returning the cid - pub fn save(&self, bs: &BS) -> Result { + /// TODO: Potentially should replaced with more targeted saving of different branches of the state tree for + /// efficiency + /// - possibly keep track of "dirty" branches that need to be flushed and put into the blockstore + /// on save + /// - hack for now is that each method that touches a hamt, will itself commit the changes to the blockstore + pub fn save(&self, bs: &BS) -> Result { let serialized = match fvm_ipld_encoding::to_vec(self) { Ok(s) => s, Err(err) => return Err(anyhow!("failed to serialize state: {:?}", err)), @@ -81,7 +92,7 @@ impl TokenState { } /// Get the balance of an ActorID from the currently stored state - pub fn get_balance( + pub fn get_balance( &self, bs: &BS, owner: ActorID, @@ -101,7 +112,7 @@ impl TokenState { /// /// Caller must ensure the requested amount is non-negative. /// Returns an error if the balance overflows, else returns the new balance - pub fn increase_balance( + pub fn increase_balance( &mut self, bs: &BS, actor: ActorID, @@ -125,16 +136,20 @@ impl TokenState { new_amount } - None => { - balance_map.set(actor, BigIntDe(value.clone()))?; - self.balances = balance_map.flush()?; - value.clone() - } + None => value.clone(), }; - // update the state with the new balance balance_map.set(actor, BigIntDe(new_balance.clone()))?; + self.balances = balance_map.flush()?; + // let serialised = match fvm_ipld_encoding::to_vec(&self.balances) { + // Ok(s) => s, + // Err(err) => return Err(anyhow!("failed to serialize state: {:?}", err)), + // }; + // bs.put_keyed(&self.balances, serialised.as_slice())?; + // HACK: This should be done at library level + bs.put_cbor(&self, Code::Blake2b256)?; + Ok(new_balance) } @@ -143,7 +158,7 @@ impl TokenState { /// Caller must ensure the requested amount is non-negative. /// Returns an error if the balance overflows, or if resulting balance would be negative. /// Else returns the new balance - pub fn decrease_balance( + pub fn decrease_balance( &mut self, bs: &BS, actor: ActorID, @@ -187,11 +202,11 @@ impl TokenState { } /// Retrieve the balance map as a HAMT - fn get_balance_map( + fn get_balance_map( &self, bs: &BS, ) -> Result> { - match Hamt::::load(&self.balances, *bs) { + match Hamt::::load(&self.balances, (*bs).clone()) { Ok(map) => Ok(map), Err(err) => return Err(anyhow!("Failed to load balances hamt: {:?}", err)), } @@ -216,7 +231,7 @@ impl TokenState { /// Get the allowance that an owner has approved for a spender /// /// If an existing allowance cannot be found, it is implicitly assumed to be zero - pub fn get_allowance_between( + pub fn get_allowance_between( &self, bs: &BS, owner: ActorID, @@ -238,7 +253,7 @@ impl TokenState { /// Increase the allowance between owner and spender by the specified value /// /// Caller must ensure that value is non-negative. - pub fn increase_allowance( + pub fn increase_allowance( &mut self, bs: &BS, owner: ActorID, @@ -287,7 +302,7 @@ impl TokenState { // If allowance map does not exist, create it and insert the allowance let mut owner_allowances = - Hamt::::new_with_bit_width(*bs, HAMT_BIT_WIDTH); + Hamt::::new_with_bit_width((*bs).clone(), HAMT_BIT_WIDTH); owner_allowances.set(spender, BigIntDe(value.clone()))?; { @@ -308,7 +323,7 @@ impl TokenState { /// /// If the allowance is decreased to zero, the entry is removed from the map. /// If the map is empty, it is removed from the root map. - pub fn decrease_allowance( + pub fn decrease_allowance( &mut self, bs: &BS, owner: ActorID, @@ -375,7 +390,7 @@ impl TokenState { /// Revokes an approved allowance by removing the entry from the owner-spender map /// /// If that map becomes empty, it is removed from the root map. - pub fn revoke_allowance( + pub fn revoke_allowance( &mut self, bs: &BS, owner: ActorID, @@ -402,7 +417,7 @@ impl TokenState { /// Atomically checks if value is less than the allowance and deducts it if so /// /// Returns new allowance if successful, else returns an error and the allowance is unchanged - pub fn attempt_use_allowance( + pub fn attempt_use_allowance( &mut self, bs: &BS, spender: u64, @@ -451,14 +466,14 @@ impl TokenState { /// Ok(Some) if the owner has allocated allowances to other actors /// Ok(None) if the owner has no current non-zero allowances to other actors /// Err if operations on the underlying Hamt failed - fn get_owner_allowance_map( + fn get_owner_allowance_map( &self, bs: &BS, owner: ActorID, ) -> Result>> { let allowances_map = self.get_allowances_map(bs)?; let owner_allowances = match allowances_map.get(&owner)? { - Some(cid) => Some(Hamt::::load(cid, *bs)?), + Some(cid) => Some(Hamt::::load(cid, (*bs).clone())?), None => None, }; Ok(owner_allowances) @@ -467,10 +482,48 @@ impl TokenState { /// Get the global allowances map /// /// Gets a HAMT with CIDs linking to other HAMTs - fn get_allowances_map(&self, bs: &BS) -> Result> { - Hamt::::load(&self.allowances, *bs) + fn get_allowances_map(&self, bs: &BS) -> Result> { + Hamt::::load(&self.allowances, (*bs).clone()) .map_err(|e| anyhow!("Failed to load base allowances map {}", e)) } } impl Cbor for TokenState {} + +#[cfg(test)] +mod test { + use fvm_shared::{ + bigint::{BigInt, Zero}, + ActorID, + }; + + use crate::blockstore::MemoryBlockstore; + + use super::TokenState; + + #[test] + fn it_instantiates() { + let bs = MemoryBlockstore::new(); + let state = TokenState::new(&bs).unwrap(); + let cid = state.save(&bs).unwrap(); + let saved_state = TokenState::load(&bs, &cid).unwrap(); + assert_eq!(state, saved_state); + } + + #[test] + fn it_increases_balance_of_new_actor() { + let bs = MemoryBlockstore::new(); + let mut state = TokenState::new(&bs).unwrap(); + + let actor: ActorID = 1; + + assert_eq!(state.get_balance(&bs, actor).unwrap(), BigInt::zero()); + + let amount = BigInt::from(100); + state.increase_balance(&bs, actor, &amount).unwrap(); + let new_cid = state.save(&bs).unwrap(); + + let state = TokenState::load(&bs, &new_cid).unwrap(); + assert_eq!(state.get_balance(&bs, actor).unwrap(), amount); + } +} diff --git a/fil_token/tests/blockstore.rs b/fil_token/tests/blockstore.rs index e5779b38..e69de29b 100644 --- a/fil_token/tests/blockstore.rs +++ b/fil_token/tests/blockstore.rs @@ -1,32 +0,0 @@ -use std::{cell::RefCell, collections::HashMap}; - -use anyhow::Result; -use cid::Cid; -use fvm_ipld_blockstore::Blockstore; - -/// An in-memory blockstore impl taken from filecoin-project/ref-fvm -#[derive(Debug, Default, Clone)] -pub struct MemoryBlockstore { - blocks: RefCell>>, -} - -impl MemoryBlockstore { - pub fn new() -> Self { - Self::default() - } -} - -impl Blockstore for MemoryBlockstore { - fn has(&self, k: &Cid) -> Result { - Ok(self.blocks.borrow().contains_key(k)) - } - - fn get(&self, k: &Cid) -> Result>> { - Ok(self.blocks.borrow().get(k).cloned()) - } - - fn put_keyed(&self, k: &Cid, block: &[u8]) -> Result<()> { - self.blocks.borrow_mut().insert(*k, block.into()); - Ok(()) - } -} diff --git a/fil_token/tests/lib.rs b/fil_token/tests/lib.rs index 14763937..d78f7aac 100644 --- a/fil_token/tests/lib.rs +++ b/fil_token/tests/lib.rs @@ -1,9 +1,6 @@ mod blockstore; mod runtime; -use blockstore::MemoryBlockstore; -use runtime::TestRuntime; - -use fil_token::token::TokenHelper; +pub use blockstore::MemoryBlockstore; #[test] fn it_mints() { From ccfb79b7794a175b7369319eb0ba2d466ec3d111 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Sun, 17 Jul 2022 23:22:02 +1000 Subject: [PATCH 20/21] HACK: save changes into blockstore properly for some operations --- fil_token/src/token/state.rs | 23 +++++++++++-------- .../actors/wfil_token_actor/src/lib.rs | 5 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/fil_token/src/token/state.rs b/fil_token/src/token/state.rs index de1fb7b1..18b6d64d 100644 --- a/fil_token/src/token/state.rs +++ b/fil_token/src/token/state.rs @@ -6,6 +6,7 @@ use cid::Cid; use fvm_ipld_blockstore::Block; use fvm_ipld_blockstore::Blockstore as IpldStore; +use fvm_ipld_encoding::ser; use fvm_ipld_encoding::tuple::*; use fvm_ipld_encoding::Cbor; use fvm_ipld_encoding::CborStore; @@ -140,16 +141,12 @@ impl TokenState { }; balance_map.set(actor, BigIntDe(new_balance.clone()))?; - self.balances = balance_map.flush()?; - // let serialised = match fvm_ipld_encoding::to_vec(&self.balances) { - // Ok(s) => s, - // Err(err) => return Err(anyhow!("failed to serialize state: {:?}", err)), - // }; - // bs.put_keyed(&self.balances, serialised.as_slice())?; - // HACK: This should be done at library level - bs.put_cbor(&self, Code::Blake2b256)?; - + let serialised = match fvm_ipld_encoding::to_vec(&balance_map) { + Ok(s) => s, + Err(err) => return Err(anyhow!("failed to serialize state: {:?}", err)), + }; + bs.put_keyed(&self.balances, &serialised)?; Ok(new_balance) } @@ -198,6 +195,12 @@ impl TokenState { balance_map.set(actor, BigIntDe(new_amount.clone()))?; self.balances = balance_map.flush()?; + let serialised = match fvm_ipld_encoding::to_vec(&balance_map) { + Ok(s) => s, + Err(err) => return Err(anyhow!("failed to serialize state: {:?}", err)), + }; + bs.put_keyed(&self.balances, &serialised)?; + Ok(new_amount) } @@ -514,9 +517,9 @@ mod test { fn it_increases_balance_of_new_actor() { let bs = MemoryBlockstore::new(); let mut state = TokenState::new(&bs).unwrap(); - let actor: ActorID = 1; + // Initially any actor has an implicit balance of 0 assert_eq!(state.get_balance(&bs, actor).unwrap(), BigInt::zero()); let amount = BigInt::from(100); diff --git a/testing/fil_token_integration/actors/wfil_token_actor/src/lib.rs b/testing/fil_token_integration/actors/wfil_token_actor/src/lib.rs index f78cd330..7816e202 100644 --- a/testing/fil_token_integration/actors/wfil_token_actor/src/lib.rs +++ b/testing/fil_token_integration/actors/wfil_token_actor/src/lib.rs @@ -1,6 +1,5 @@ use anyhow::Result; use fil_token::blockstore::Blockstore; -use fil_token::runtime::FvmRuntime; use fil_token::token::*; use fvm_ipld_encoding::{RawBytes, DAG_CBOR}; use fvm_sdk as sdk; @@ -10,7 +9,7 @@ use sdk::NO_DATA_BLOCK_ID; struct WfilToken { /// Default token helper impl - util: TokenHelper, + util: TokenHelper, } /// Implement the token API @@ -77,7 +76,7 @@ pub fn invoke(params: u32) -> u32 { let root_cid = sdk::sself::root().unwrap(); let token_actor = WfilToken { - util: TokenHelper::new(Blockstore {}, FvmRuntime {}, root_cid), + util: TokenHelper::new(Blockstore {}, root_cid), }; //TODO: this internal dispatch can be pushed as a library function into the fil_token crate From 74e6738efe2801835990f4bf297cb4b132b7c836 Mon Sep 17 00:00:00 2001 From: Alex Su Date: Mon, 18 Jul 2022 11:31:40 +1000 Subject: [PATCH 21/21] remove unecessary runtime abstraction --- fil_token/src/lib.rs | 1 - fil_token/src/runtime/fvm.rs | 19 ------------------- fil_token/src/runtime/mod.rs | 15 --------------- fil_token/tests/blockstore.rs | 0 fil_token/tests/lib.rs | 8 -------- fil_token/tests/runtime.rs | 20 -------------------- 6 files changed, 63 deletions(-) delete mode 100644 fil_token/src/runtime/fvm.rs delete mode 100644 fil_token/src/runtime/mod.rs delete mode 100644 fil_token/tests/blockstore.rs delete mode 100644 fil_token/tests/lib.rs delete mode 100644 fil_token/tests/runtime.rs diff --git a/fil_token/src/lib.rs b/fil_token/src/lib.rs index 3ff6b814..a6309255 100644 --- a/fil_token/src/lib.rs +++ b/fil_token/src/lib.rs @@ -1,5 +1,4 @@ pub mod blockstore; -pub mod runtime; pub mod token; #[cfg(test)] diff --git a/fil_token/src/runtime/fvm.rs b/fil_token/src/runtime/fvm.rs deleted file mode 100644 index 3bac81b1..00000000 --- a/fil_token/src/runtime/fvm.rs +++ /dev/null @@ -1,19 +0,0 @@ -use super::Runtime; - -use anyhow::{anyhow, Result}; -use fvm_sdk as sdk; -use sdk::actor; -use sdk::message; - -/// Provides access to the FVM which acts as the runtime for actors deployed on-chain -pub struct FvmRuntime {} - -impl Runtime for FvmRuntime { - fn caller(&self) -> u64 { - message::caller() - } - - fn resolve_address(&self, addr: &fvm_shared::address::Address) -> Result { - actor::resolve_address(addr).ok_or_else(|| anyhow!("Failed to resolve address")) - } -} diff --git a/fil_token/src/runtime/mod.rs b/fil_token/src/runtime/mod.rs deleted file mode 100644 index 4b5d5231..00000000 --- a/fil_token/src/runtime/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod fvm; -pub use fvm::*; - -use anyhow::Result; -use fvm_shared::address::Address; - -/// Abstraction of the runtime that an actor is executed in, providing access to syscalls and -/// features of the FVM -pub trait Runtime { - /// Get the direct-caller that invoked the current actor - fn caller(&self) -> u64; - - /// Attempts to resolve an address to an ActorID - fn resolve_address(&self, addr: &Address) -> Result; -} diff --git a/fil_token/tests/blockstore.rs b/fil_token/tests/blockstore.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/fil_token/tests/lib.rs b/fil_token/tests/lib.rs deleted file mode 100644 index d78f7aac..00000000 --- a/fil_token/tests/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod blockstore; -mod runtime; -pub use blockstore::MemoryBlockstore; - -#[test] -fn it_mints() { - // let token = TokenHelper::new(MemoryBlockstore::new(), TestRuntime::new(1)); -} diff --git a/fil_token/tests/runtime.rs b/fil_token/tests/runtime.rs deleted file mode 100644 index 199f27b2..00000000 --- a/fil_token/tests/runtime.rs +++ /dev/null @@ -1,20 +0,0 @@ -use fil_token::runtime::Runtime; -pub struct TestRuntime { - caller: u64, -} - -impl TestRuntime { - pub fn new(caller: u64) -> Self { - Self { caller } - } -} - -impl Runtime for TestRuntime { - fn caller(&self) -> u64 { - return self.caller; - } - - fn resolve_address(&self, addr: &fvm_shared::address::Address) -> anyhow::Result { - Ok(addr.id().unwrap()) - } -}