diff --git a/core/application/src/env.rs b/core/application/src/env.rs index 2064ef6ed..16a790ee3 100644 --- a/core/application/src/env.rs +++ b/core/application/src/env.rs @@ -242,6 +242,8 @@ impl ApplicationEnv { metadata_table.insert(Metadata::BlockNumber, Value::BlockNumber(0)); + metadata_table.insert(Metadata::WithdrawId, Value::WithdrawId(0)); + metadata_table.insert( Metadata::ProtocolFundAddress, Value::AccountPublicKey(genesis.protocol_fund_address), diff --git a/core/application/src/state/executor.rs b/core/application/src/state/executor.rs index 81944f1c2..de0c6dcd2 100644 --- a/core/application/src/state/executor.rs +++ b/core/application/src/state/executor.rs @@ -131,6 +131,8 @@ pub struct StateExecutor { ), >, pub committee_selection_beacon_non_revealing_node: B::Ref, + pub flk_withdraws: B::Ref)>, + pub usdc_withdraws: B::Ref)>, pub backend: B, } @@ -162,6 +164,8 @@ impl StateExecutor { committee_selection_beacon: backend.get_table_reference("committee_selection_beacon"), committee_selection_beacon_non_revealing_node: backend .get_table_reference("committee_selection_beacon_non_revealing_node"), + flk_withdraws: backend.get_table_reference("flk_withdraws"), + usdc_withdraws: backend.get_table_reference("usdc_withdraws"), backend, } } @@ -466,12 +470,53 @@ impl StateExecutor { fn withdraw( &self, - _sender: TransactionSender, - _reciever: EthAddress, - _amount: HpUfixed<18>, - _token: Tokens, + sender: TransactionSender, + reciever: EthAddress, + amount: HpUfixed<18>, + token: Tokens, ) -> TransactionResponse { - TransactionResponse::Revert(ExecutionError::Unimplemented) + // This transaction is only callable by AccountOwners and not nodes + // So revert if the sender is a node public key + let sender = match self.only_account_owner(sender) { + Ok(account) => account, + Err(e) => return e, + }; + let Some(mut account) = self.account_info.get(&sender) else { + return TransactionResponse::Revert(ExecutionError::AccountDoesNotExist); + }; + + let withdraw_id = match self.metadata.get(&Metadata::WithdrawId) { + Some(Value::WithdrawId(epoch)) => epoch, + _ => 0, + }; + + match token { + Tokens::FLK => { + if amount > account.flk_balance { + return TransactionResponse::Revert(ExecutionError::InsufficientBalance); + } + + account.flk_balance -= amount.clone(); + self.flk_withdraws.set(withdraw_id, (reciever, amount)); + self.metadata + .set(Metadata::WithdrawId, Value::WithdrawId(withdraw_id + 1)); + }, + Tokens::USDC => { + // TODO(matthias): make sure that this conversion is safe + let amount = amount.convert_precision::<6>(); + if amount > account.stables_balance { + return TransactionResponse::Revert(ExecutionError::InsufficientBalance); + } + + account.stables_balance -= amount.clone(); + self.usdc_withdraws.set(withdraw_id, (reciever, amount)); + self.metadata + .set(Metadata::WithdrawId, Value::WithdrawId(withdraw_id + 1)); + }, + } + + self.account_info.set(sender, account); + TransactionResponse::Success(ExecutionData::None) } fn deposit( diff --git a/core/application/src/state/executor/epoch_change.rs b/core/application/src/state/executor/epoch_change.rs index 591a4db2a..c4a6573ae 100644 --- a/core/application/src/state/executor/epoch_change.rs +++ b/core/application/src/state/executor/epoch_change.rs @@ -676,6 +676,12 @@ impl StateExecutor { // Clear executed digests. self.executed_digests.clear(); + // Clear withdraws + self.flk_withdraws.clear(); + self.usdc_withdraws.clear(); + self.metadata + .set(Metadata::WithdrawId, Value::WithdrawId(0)); + self.committee_info.set(epoch, current_committee); // Get new committee let new_committee = self.choose_new_committee(beacons); diff --git a/core/application/src/state/query.rs b/core/application/src/state/query.rs index 759d64fce..102fa79e8 100644 --- a/core/application/src/state/query.rs +++ b/core/application/src/state/query.rs @@ -79,6 +79,8 @@ pub struct QueryRunner { ), >, committee_selection_beacon_non_revealing_node: ResolvedTableReference, + flk_withdraws: ResolvedTableReference)>, + usdc_withdraws: ResolvedTableReference)>, } impl QueryRunner { @@ -122,7 +124,8 @@ impl SyncQueryRunnerInterface for QueryRunner { )>("committee_selection_beacon"), committee_selection_beacon_non_revealing_node: atomo .resolve::("committee_selection_beacon_non_revealing_node"), - + flk_withdraws: atomo.resolve::)>("flk_withdraws"), + usdc_withdraws: atomo.resolve::)>("usdc_withdraws"), inner: atomo, } } @@ -372,4 +375,20 @@ impl SyncQueryRunnerInterface for QueryRunner { // This is consistent with the logic in `Env::apply_genesis_block`. self.get_metadata(&Metadata::Epoch).is_some() } + + fn get_flk_withdraws(&self) -> Vec<(u64, EthAddress, HpUfixed<18>)> { + self.inner + .run(|ctx| self.flk_withdraws.get(ctx).as_map()) + .iter() + .map(|(id, (address, amount))| (*id, *address, amount.clone())) + .collect() + } + + fn get_usdc_withdraws(&self) -> Vec<(u64, EthAddress, HpUfixed<6>)> { + self.inner + .run(|ctx| self.usdc_withdraws.get(ctx).as_map()) + .iter() + .map(|(id, (address, amount))| (*id, *address, amount.clone())) + .collect() + } } diff --git a/core/application/src/state/writer.rs b/core/application/src/state/writer.rs index c2b90df15..898aa8e79 100644 --- a/core/application/src/state/writer.rs +++ b/core/application/src/state/writer.rs @@ -188,6 +188,8 @@ impl ApplicationState { Option, )>("committee_selection_beacon") .with_table::("committee_selection_beacon_non_revealing_node") + .with_table::)>("flk_withdraws") + .with_table::)>("usdc_withdraws") .enable_iter("current_epoch_served") .enable_iter("rep_measurements") .enable_iter("submitted_rep_measurements") @@ -200,7 +202,9 @@ impl ApplicationState { .enable_iter("uri_to_node") .enable_iter("node_to_uri") .enable_iter("committee_selection_beacon") - .enable_iter("committee_selection_beacon_non_revealing_node"); + .enable_iter("committee_selection_beacon_non_revealing_node") + .enable_iter("flk_withdraws") + .enable_iter("usdc_withdraws"); #[cfg(debug_assertions)] { diff --git a/core/application/src/tests/balances.rs b/core/application/src/tests/balances.rs index 480de5cfb..7a5f1614f 100644 --- a/core/application/src/tests/balances.rs +++ b/core/application/src/tests/balances.rs @@ -3,10 +3,12 @@ use hp_fixed::unsigned::HpUfixed; use lightning_interfaces::types::{ ExecutionData, ExecutionError, + GenesisAccount, ProofOfConsensus, Tokens, UpdateMethod, }; +use lightning_interfaces::SyncQueryRunnerInterface; use tempfile::tempdir; use super::utils::*; @@ -221,3 +223,75 @@ async fn test_deposit_usdc_works_properly() { intial_balance + deposit_amount ); } + +#[tokio::test] +async fn test_withdraw_usdc_works_properly() { + let temp_dir = tempdir().unwrap(); + + let mut genesis = test_genesis(); + + let owner_secret_key = AccountOwnerSecretKey::generate(); + let owner: EthAddress = owner_secret_key.to_pk().into(); + + let receiver_secret_key = AccountOwnerSecretKey::generate(); + let receiver: EthAddress = receiver_secret_key.to_pk().into(); + + let account = GenesisAccount { + public_key: owner, + flk_balance: 0_u64.into(), + stables_balance: 1000, + bandwidth_balance: 0, + }; + genesis.account = vec![account]; + + let (update_socket, query_runner) = init_app_with_genesis(&temp_dir, &genesis); + + let withdraw_amount = 500_u64; + let withdraw = UpdateMethod::Withdraw { + amount: withdraw_amount.into(), + token: Tokens::USDC, + receiving_address: receiver, + }; + let update = prepare_update_request_account(withdraw, &owner_secret_key, 1); + expect_tx_success(update, &update_socket, ExecutionData::None).await; + + let withdraws = query_runner.get_usdc_withdraws(); + assert_eq!(withdraws[0].1, receiver); + assert_eq!(withdraws[0].2, withdraw_amount.into()); +} + +#[tokio::test] +async fn test_withdraw_flk_works_properly() { + let temp_dir = tempdir().unwrap(); + + let mut genesis = test_genesis(); + + let owner_secret_key = AccountOwnerSecretKey::generate(); + let owner: EthAddress = owner_secret_key.to_pk().into(); + + let receiver_secret_key = AccountOwnerSecretKey::generate(); + let receiver: EthAddress = receiver_secret_key.to_pk().into(); + + let account = GenesisAccount { + public_key: owner, + flk_balance: 1000_u64.into(), + stables_balance: 0, + bandwidth_balance: 0, + }; + genesis.account = vec![account]; + + let (update_socket, query_runner) = init_app_with_genesis(&temp_dir, &genesis); + + let withdraw_amount = 500_u64; + let withdraw = UpdateMethod::Withdraw { + amount: withdraw_amount.into(), + token: Tokens::FLK, + receiving_address: receiver, + }; + let update = prepare_update_request_account(withdraw, &owner_secret_key, 1); + expect_tx_success(update, &update_socket, ExecutionData::None).await; + + let withdraws = query_runner.get_flk_withdraws(); + assert_eq!(withdraws[0].1, receiver); + assert_eq!(withdraws[0].2, withdraw_amount.into()); +} diff --git a/core/interfaces/src/application.rs b/core/interfaces/src/application.rs index a39bfd9c9..03bdfe086 100644 --- a/core/interfaces/src/application.rs +++ b/core/interfaces/src/application.rs @@ -8,6 +8,7 @@ use atomo::{Atomo, InMemoryStorage, KeyIterator, QueryPerm, StorageBackend}; use fdi::BuildGraph; use fleek_crypto::{ClientPublicKey, EthAddress, NodePublicKey}; use fxhash::FxHashMap; +use hp_fixed::unsigned::HpUfixed; use lightning_types::{ AccountInfo, Blake3Hash, @@ -240,6 +241,12 @@ pub trait SyncQueryRunnerInterface: Clone + Send + Sync + 'static { // Returns whether the genesis block has been applied. fn has_genesis(&self) -> bool; + + /// Returns a list of FLK withdraws + fn get_flk_withdraws(&self) -> Vec<(u64, EthAddress, HpUfixed<18>)>; + + /// Returns a list of USDC withdraws + fn get_usdc_withdraws(&self) -> Vec<(u64, EthAddress, HpUfixed<6>)>; } #[derive(Clone, Debug)] diff --git a/core/types/src/response.rs b/core/types/src/response.rs index b9c01a816..b8e16f759 100644 --- a/core/types/src/response.rs +++ b/core/types/src/response.rs @@ -144,6 +144,7 @@ pub enum ExecutionError { NotNodeOwner, NotCommitteeMember, NodeDoesNotExist, + AccountDoesNotExist, CantSendToYourself, AlreadySignaled, SubmittedTooManyTransactions, diff --git a/core/types/src/state.rs b/core/types/src/state.rs index 4707554e4..ec6a53f43 100644 --- a/core/types/src/state.rs +++ b/core/types/src/state.rs @@ -107,6 +107,7 @@ pub enum Metadata { CommitteeSelectionBeaconPhase, CommitteeSelectionBeaconRound, EpochEra, + WithdrawId, } /// The Value enum is a data type used to represent values in a key-value pair for a metadata table @@ -127,6 +128,7 @@ pub enum Value { CommitteeSelectionBeaconPhase(CommitteeSelectionBeaconPhase), CommitteeSelectionBeaconRound(CommitteeSelectionBeaconRound), EpochEra(u64), + WithdrawId(u64), } impl Value {