diff --git a/pallets/duniter-account/src/lib.rs b/pallets/duniter-account/src/lib.rs index 1b7f59f8a19933cae41c313f5600e9663060c4d5..bfe802c11b9ef7d92f1b59589a78414e10fb443d 100644 --- a/pallets/duniter-account/src/lib.rs +++ b/pallets/duniter-account/src/lib.rs @@ -148,8 +148,8 @@ pub mod pallet { }, OneshotAccountConsumed { account: T::AccountId, - balance: T::Balance, - dest: T::AccountId, + dest1: (T::AccountId, T::Balance), + dest2: Option<(T::AccountId, T::Balance)>, }, /// Random id assigned /// [account_id, random_id] @@ -160,15 +160,19 @@ pub mod pallet { #[pallet::error] pub enum Error<T> { - /// DestAccountNotExist + /// Block height is in the future + BlockHeightInFuture, + /// Block height is too old + BlockHeightTooOld, + /// Destination account does not exist DestAccountNotExist, - /// ExistentialDeposit + /// Destination account has balance less than existential deposit ExistentialDeposit, - /// InsufficientBalance + /// Source account has insufficient balance InsufficientBalance, - /// OneshotAccouncAlreadyCreated + /// Destination oneshot account already exists OneshotAccountAlreadyCreated, - /// OneshotAccountNotExist + /// Source oneshot account does not exist OneshotAccountNotExist, } @@ -254,6 +258,10 @@ pub mod pallet { // CALLS // #[pallet::call] impl<T: Config> Pallet<T> { + /// Create an account that can only be consumed once + /// + /// - `dest`: The oneshot account to be created. + /// - `balance`: The balance to be transfered to this oneshot account. #[pallet::weight(500_000_000)] pub fn create_oneshot_account( origin: OriginFor<T>, @@ -292,9 +300,14 @@ pub mod pallet { Ok(()) } + /// Consume a oneshot account and transfer its balance to an account + /// + /// - `block_height`: must be a recent block number. The limit is `BlockHashCount` in the past. (this is to prevent replay attacks) + /// - `dest`: `dest.1` is the destination account. If `dest.0` is `true`, then a oneshot account is created at `dest.1`. Else, `dest.1` has to be an existing account. #[pallet::weight(500_000_000)] pub fn consume_oneshot_account( origin: OriginFor<T>, + block_height: T::BlockNumber, dest: (bool, <T::Lookup as StaticLookup>::Source), ) -> DispatchResult { let transactor = ensure_signed(origin)?; @@ -304,31 +317,121 @@ pub mod pallet { let value = OneshotAccounts::<T>::take(&transactor) .ok_or(Error::<T>::OneshotAccountNotExist)?; + ensure!( + block_height <= frame_system::Pallet::<T>::block_number(), + Error::<T>::BlockHeightInFuture + ); + ensure!( + frame_system::pallet::BlockHash::<T>::contains_key(block_height), + Error::<T>::BlockHeightTooOld + ); if dest_is_oneshot { ensure!( OneshotAccounts::<T>::get(&dest).is_none(), Error::<T>::OneshotAccountAlreadyCreated ); OneshotAccounts::<T>::insert(&dest, value); - Self::deposit_event(Event::OneshotAccountConsumed { - account: transactor.clone(), - balance: value, - dest: dest.clone(), - }); Self::deposit_event(Event::OneshotAccountCreated { - account: dest, + account: dest.clone(), balance: value, - creator: transactor, + creator: transactor.clone(), }); } else { let dest_data = frame_system::Account::<T>::get(&dest).data; ensure!(dest_data.was_providing(), Error::<T>::DestAccountNotExist); - Self::deposit_event(Event::OneshotAccountConsumed { - account: transactor, - balance: value, - dest, + frame_system::Account::<T>::mutate(&dest, |a| a.data.add_free(value)); + } + OneshotAccounts::<T>::remove(&transactor); + Self::deposit_event(Event::OneshotAccountConsumed { + account: transactor, + dest1: (dest, value), + dest2: None, + }); + + Ok(()) + } + /// Consume a oneshot account and transfer its balance to two accounts + /// + /// - `block_height`: must be a recent block number. The limit is `BlockHashCount` in the past. (this is to prevent replay attacks) + /// - `dest1`: `dest1.1` is the destination account. If `dest1.0` is `true`, then a oneshot account is created at `dest1.1`. Else, `dest1.1` has to be an existing account. + /// - `dest2`: Idem. + /// - `balance1`: The amount transfered to `dest1`, the leftover being transfered to `dest2`. + #[pallet::weight(500_000_000)] + pub fn consume_oneshot_account_two_dests( + origin: OriginFor<T>, + block_height: T::BlockNumber, + dest1: (bool, <T::Lookup as StaticLookup>::Source), + dest2: (bool, <T::Lookup as StaticLookup>::Source), + #[pallet::compact] balance1: T::Balance, + ) -> DispatchResult { + let transactor = ensure_signed(origin)?; + let dest1_is_oneshot = dest1.0; + let dest1 = T::Lookup::lookup(dest1.1)?; + let dest2_is_oneshot = dest2.0; + let dest2 = T::Lookup::lookup(dest2.1)?; + + let value = OneshotAccounts::<T>::take(&transactor) + .ok_or(Error::<T>::OneshotAccountNotExist)?; + + ensure!(value > balance1, Error::<T>::InsufficientBalance); + let balance2 = value.saturating_sub(balance1); + ensure!( + block_height <= frame_system::Pallet::<T>::block_number(), + Error::<T>::BlockHeightInFuture + ); + ensure!( + frame_system::pallet::BlockHash::<T>::contains_key(block_height), + Error::<T>::BlockHeightTooOld + ); + if dest1_is_oneshot { + ensure!( + OneshotAccounts::<T>::get(&dest1).is_none(), + Error::<T>::OneshotAccountAlreadyCreated + ); + ensure!( + balance1 >= T::ExistentialDeposit::get(), + Error::<T>::ExistentialDeposit + ); + } else { + let dest1_data = frame_system::Account::<T>::get(&dest1).data; + ensure!(dest1_data.was_providing(), Error::<T>::DestAccountNotExist); + } + if dest2_is_oneshot { + ensure!( + OneshotAccounts::<T>::get(&dest2).is_none(), + Error::<T>::OneshotAccountAlreadyCreated + ); + ensure!( + balance2 >= T::ExistentialDeposit::get(), + Error::<T>::ExistentialDeposit + ); + OneshotAccounts::<T>::insert(&dest2, balance2); + Self::deposit_event(Event::OneshotAccountCreated { + account: dest2.clone(), + balance: balance2, + creator: transactor.clone(), }); + } else { + let dest2_data = frame_system::Account::<T>::get(&dest2).data; + ensure!(dest2_data.was_providing(), Error::<T>::DestAccountNotExist); + frame_system::Account::<T>::mutate(&dest2, |a| a.data.add_free(balance2)); } + if dest1_is_oneshot { + OneshotAccounts::<T>::insert(&dest1, balance1); + Self::deposit_event(Event::OneshotAccountCreated { + account: dest1.clone(), + balance: balance1, + creator: transactor.clone(), + }); + } else { + frame_system::Account::<T>::mutate(&dest1, |a| a.data.add_free(balance1)); + } + OneshotAccounts::<T>::remove(&transactor); + Self::deposit_event(Event::OneshotAccountConsumed { + account: transactor, + dest1: (dest1, balance1), + dest2: Some((dest2, balance2)), + }); Ok(()) } diff --git a/pallets/duniter-account/src/types.rs b/pallets/duniter-account/src/types.rs index 8f1a6b16bbba8ac057724c09b5de640a12d70d64..486be537bf5b7b2797f089304e4d37b0f29b21a9 100644 --- a/pallets/duniter-account/src/types.rs +++ b/pallets/duniter-account/src/types.rs @@ -29,6 +29,9 @@ pub struct AccountData<Balance> { } impl<Balance: Copy + Saturating + Zero> AccountData<Balance> { + pub fn add_free(&mut self, amount: Balance) { + self.free = self.free.saturating_add(amount); + } pub fn free_and_reserved(&self) -> Balance { self.free.saturating_add(self.reserved) } diff --git a/runtime/gdev/tests/integration_tests.rs b/runtime/gdev/tests/integration_tests.rs index 6ffc2f586ac041a0875188afb00441dcc62a753b..014b52940796e2c50e939e64db85daaa2cce19a1 100644 --- a/runtime/gdev/tests/integration_tests.rs +++ b/runtime/gdev/tests/integration_tests.rs @@ -344,3 +344,78 @@ fn test_create_new_idty() { ); }); } + +#[test] +fn test_oneshot_accounts() { + ExtBuilder::new(1, 3, 4) + .with_initial_balances(vec![(AccountKeyring::Alice.to_account_id(), 1_000)]) + .build() + .execute_with(|| { + run_to_block(6); + + assert_ok!(Account::create_oneshot_account( + frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(), + MultiAddress::Id(AccountKeyring::Eve.to_account_id()), + 400 + )); + assert_eq!( + Balances::free_balance(AccountKeyring::Alice.to_account_id()), + 600 + ); + run_to_block(7); + + assert_ok!(Account::consume_oneshot_account_two_dests( + frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), + 0, + ( + true, + MultiAddress::Id(AccountKeyring::Ferdie.to_account_id()) + ), + ( + false, + MultiAddress::Id(AccountKeyring::Alice.to_account_id()) + ), + 300 + )); + assert_eq!( + Balances::free_balance(AccountKeyring::Alice.to_account_id()), + 700 + ); + assert_err!( + Account::consume_oneshot_account( + frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), + 0, + ( + true, + MultiAddress::Id(AccountKeyring::Ferdie.to_account_id()) + ), + ), + pallet_duniter_account::Error::<Runtime>::OneshotAccountNotExist + ); + run_to_block(8); + + assert_ok!(Account::consume_oneshot_account( + frame_system::RawOrigin::Signed(AccountKeyring::Ferdie.to_account_id()).into(), + 0, + ( + false, + MultiAddress::Id(AccountKeyring::Alice.to_account_id()) + ), + )); + assert_eq!( + Balances::free_balance(AccountKeyring::Alice.to_account_id()), + 1000 + ); + assert_err!( + Account::consume_oneshot_account( + frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), + 0, + ( + false, + MultiAddress::Id(AccountKeyring::Alice.to_account_id()) + ), + ), + pallet_duniter_account::Error::<Runtime>::OneshotAccountNotExist + ); + }); +}