3139ffa25e
- snowbridge-pezpallet-* → pezsnowbridge-pezpallet-* (201 refs) - pallet/ directories → pezpallet/ (4 locations) - Fixed pezpallet.rs self-include recursion bug - Fixed sc-chain-spec hardcoded crate name in derive macro - Reverted .pezpallet_by_name() to .pallet_by_name() (subxt API) - Added BizinikiwiConfig type alias for zombienet tests - Deleted obsolete session state files Verified: pezsnowbridge-pezpallet-*, pezpallet-staking, pezpallet-staking-async, pezframe-benchmarking-cli all pass cargo check
391 lines
13 KiB
Rust
391 lines
13 KiB
Rust
// This file is part of Bizinikiwi.
|
|
|
|
// Copyright (C) Parity Technologies (UK) Ltd.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! Transfer with dust functionality for pezpallet-revive.
|
|
|
|
use crate::{
|
|
address::AddressMapper, exec::AccountIdOf, primitives::BalanceWithDust, storage::AccountInfo,
|
|
AccountInfoOf, BalanceOf, Config, Error, LOG_TARGET,
|
|
};
|
|
use pezframe_support::{
|
|
dispatch::DispatchResult,
|
|
traits::{
|
|
fungible::Mutate,
|
|
tokens::{Fortitude, Precision, Preservation},
|
|
Get,
|
|
},
|
|
};
|
|
|
|
/// Transfer balance between two accounts.
|
|
fn transfer_balance<T: Config>(
|
|
from: &AccountIdOf<T>,
|
|
to: &AccountIdOf<T>,
|
|
value: BalanceOf<T>,
|
|
preservation: Preservation,
|
|
) -> DispatchResult {
|
|
T::Currency::transfer(from, to, value, preservation)
|
|
.map_err(|err| {
|
|
log::debug!(target: LOG_TARGET, "Transfer failed: from {from:?} to {to:?} (value: ${value:?}). Err: {err:?}");
|
|
Error::<T>::TransferFailed
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Transfer dust between two account infos.
|
|
fn transfer_dust<T: Config>(
|
|
from: &mut AccountInfo<T>,
|
|
to: &mut AccountInfo<T>,
|
|
dust: u32,
|
|
) -> DispatchResult {
|
|
from.dust = from.dust.checked_sub(dust).ok_or_else(|| Error::<T>::TransferFailed)?;
|
|
to.dust = to.dust.checked_add(dust).ok_or_else(|| Error::<T>::TransferFailed)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Ensure an account has sufficient dust to perform an operation.
|
|
///
|
|
/// If the account doesn't have enough dust, this function will burn one unit of the native
|
|
/// currency (1 plank) and convert it to dust by adding `NativeToEthRatio` worth of dust
|
|
/// to the account's dust balance.
|
|
fn ensure_sufficient_dust<T: Config>(
|
|
from: &AccountIdOf<T>,
|
|
from_info: &mut AccountInfo<T>,
|
|
required_dust: u32,
|
|
) -> DispatchResult {
|
|
if from_info.dust >= required_dust {
|
|
return Ok(());
|
|
}
|
|
|
|
let plank = T::NativeToEthRatio::get();
|
|
|
|
T::Currency::burn_from(
|
|
from,
|
|
1u32.into(),
|
|
Preservation::Preserve,
|
|
Precision::Exact,
|
|
Fortitude::Polite,
|
|
)
|
|
.map_err(|err| {
|
|
log::debug!(target: LOG_TARGET, "Burning 1 plank from {from:?} failed. Err: {err:?}");
|
|
Error::<T>::TransferFailed
|
|
})?;
|
|
|
|
from_info.dust = from_info.dust.checked_add(plank).ok_or_else(|| Error::<T>::TransferFailed)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Transfer a balance with dust between two accounts.
|
|
pub(crate) fn transfer_with_dust<T: Config>(
|
|
from: &AccountIdOf<T>,
|
|
to: &AccountIdOf<T>,
|
|
value: BalanceWithDust<BalanceOf<T>>,
|
|
preservation: Preservation,
|
|
) -> DispatchResult {
|
|
let from_addr = <T::AddressMapper as AddressMapper<T>>::to_address(from);
|
|
let mut from_info = AccountInfoOf::<T>::get(&from_addr).unwrap_or_default();
|
|
|
|
if from_info.balance(from, preservation) < value {
|
|
log::debug!(target: LOG_TARGET, "Insufficient balance: from {from:?} to {to:?} (value: ${value:?}). Balance: ${:?}", from_info.balance(from, preservation));
|
|
return Err(Error::<T>::TransferFailed.into());
|
|
} else if from == to || value.is_zero() {
|
|
return Ok(());
|
|
}
|
|
|
|
let (value, dust) = value.deconstruct();
|
|
if dust == 0 {
|
|
return transfer_balance::<T>(from, to, value, preservation);
|
|
}
|
|
|
|
let to_addr = <T::AddressMapper as AddressMapper<T>>::to_address(to);
|
|
let mut to_info = AccountInfoOf::<T>::get(&to_addr).unwrap_or_default();
|
|
|
|
ensure_sufficient_dust::<T>(from, &mut from_info, dust)?;
|
|
transfer_balance::<T>(from, to, value, preservation)?;
|
|
transfer_dust::<T>(&mut from_info, &mut to_info, dust)?;
|
|
|
|
let plank = T::NativeToEthRatio::get();
|
|
if to_info.dust >= plank {
|
|
T::Currency::mint_into(to, 1u32.into())?;
|
|
to_info.dust = to_info.dust.checked_sub(plank).ok_or_else(|| Error::<T>::TransferFailed)?;
|
|
}
|
|
|
|
AccountInfoOf::<T>::set(&from_addr, Some(from_info));
|
|
AccountInfoOf::<T>::set(&to_addr, Some(to_info));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Burn a balance with dust from an account.
|
|
pub(crate) fn burn_with_dust<T: Config>(
|
|
from: &AccountIdOf<T>,
|
|
value: BalanceWithDust<BalanceOf<T>>,
|
|
) -> DispatchResult {
|
|
let from_addr = <T::AddressMapper as AddressMapper<T>>::to_address(from);
|
|
let mut from_info = AccountInfoOf::<T>::get(&from_addr).unwrap_or_default();
|
|
|
|
if from_info.balance(from, Preservation::Preserve) < value {
|
|
log::debug!(target: LOG_TARGET, "Insufficient balance: from {from:?} (value: ${value:?}). Balance: ${:?}", from_info.balance(from, Preservation::Preserve));
|
|
return Err(Error::<T>::TransferFailed.into());
|
|
} else if value.is_zero() {
|
|
return Ok(());
|
|
}
|
|
|
|
let (value, dust) = value.deconstruct();
|
|
if dust == 0 {
|
|
// No dust to handle, just burn the balance
|
|
T::Currency::burn_from(
|
|
from,
|
|
value,
|
|
Preservation::Preserve,
|
|
Precision::Exact,
|
|
Fortitude::Polite,
|
|
)
|
|
.map_err(|err| {
|
|
log::debug!(target: LOG_TARGET, "Burning {value:?} from {from:?} failed. Err: {err:?}");
|
|
Error::<T>::TransferFailed
|
|
})?;
|
|
return Ok(());
|
|
}
|
|
|
|
ensure_sufficient_dust::<T>(from, &mut from_info, dust)?;
|
|
T::Currency::burn_from(
|
|
from,
|
|
value,
|
|
Preservation::Preserve,
|
|
Precision::Exact,
|
|
Fortitude::Polite,
|
|
)
|
|
.map_err(|err| {
|
|
log::debug!(target: LOG_TARGET, "Burning {value:?} from {from:?} failed. Err: {err:?}");
|
|
Error::<T>::TransferFailed
|
|
})?;
|
|
|
|
from_info.dust = from_info.dust.checked_sub(dust).ok_or_else(|| Error::<T>::TransferFailed)?;
|
|
AccountInfoOf::<T>::set(&from_addr, Some(from_info));
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{
|
|
test_utils::{ALICE_ADDR, BOB_ADDR},
|
|
tests::{builder, test_utils::set_balance_with_dust, ExtBuilder, Test},
|
|
Config, Error, Pezpallet, H160,
|
|
};
|
|
use pezframe_support::{assert_err, traits::Get};
|
|
use pezsp_runtime::{traits::Zero, DispatchError};
|
|
|
|
#[test]
|
|
fn transfer_with_dust_works() {
|
|
struct TestCase {
|
|
description: &'static str,
|
|
from: H160,
|
|
to: H160,
|
|
from_balance: BalanceWithDust<u64>,
|
|
to_balance: BalanceWithDust<u64>,
|
|
amount: BalanceWithDust<u64>,
|
|
expected_from_balance: BalanceWithDust<u64>,
|
|
expected_to_balance: BalanceWithDust<u64>,
|
|
total_issuance_diff: i64,
|
|
expected_error: Option<DispatchError>,
|
|
}
|
|
|
|
let plank: u32 = <Test as Config>::NativeToEthRatio::get();
|
|
|
|
let test_cases = vec![
|
|
TestCase {
|
|
description: "without dust",
|
|
from: ALICE_ADDR,
|
|
to: BOB_ADDR,
|
|
from_balance: BalanceWithDust::new_unchecked::<Test>(100, 0),
|
|
to_balance: BalanceWithDust::new_unchecked::<Test>(0, 0),
|
|
amount: BalanceWithDust::new_unchecked::<Test>(1, 0),
|
|
expected_from_balance: BalanceWithDust::new_unchecked::<Test>(99, 0),
|
|
expected_to_balance: BalanceWithDust::new_unchecked::<Test>(1, 0),
|
|
total_issuance_diff: 0,
|
|
expected_error: None,
|
|
},
|
|
TestCase {
|
|
description: "with dust",
|
|
from: ALICE_ADDR,
|
|
to: BOB_ADDR,
|
|
from_balance: BalanceWithDust::new_unchecked::<Test>(100, 0),
|
|
to_balance: BalanceWithDust::new_unchecked::<Test>(0, 0),
|
|
amount: BalanceWithDust::new_unchecked::<Test>(1, 10),
|
|
expected_from_balance: BalanceWithDust::new_unchecked::<Test>(98, plank - 10),
|
|
expected_to_balance: BalanceWithDust::new_unchecked::<Test>(1, 10),
|
|
total_issuance_diff: 1,
|
|
expected_error: None,
|
|
},
|
|
TestCase {
|
|
description: "just dust",
|
|
from: ALICE_ADDR,
|
|
to: BOB_ADDR,
|
|
from_balance: BalanceWithDust::new_unchecked::<Test>(100, 0),
|
|
to_balance: BalanceWithDust::new_unchecked::<Test>(0, 0),
|
|
amount: BalanceWithDust::new_unchecked::<Test>(0, 10),
|
|
expected_from_balance: BalanceWithDust::new_unchecked::<Test>(99, plank - 10),
|
|
expected_to_balance: BalanceWithDust::new_unchecked::<Test>(0, 10),
|
|
total_issuance_diff: 1,
|
|
expected_error: None,
|
|
},
|
|
TestCase {
|
|
description: "with existing dust",
|
|
from: ALICE_ADDR,
|
|
to: BOB_ADDR,
|
|
from_balance: BalanceWithDust::new_unchecked::<Test>(100, 5),
|
|
to_balance: BalanceWithDust::new_unchecked::<Test>(0, plank - 5),
|
|
amount: BalanceWithDust::new_unchecked::<Test>(1, 10),
|
|
expected_from_balance: BalanceWithDust::new_unchecked::<Test>(98, plank - 5),
|
|
expected_to_balance: BalanceWithDust::new_unchecked::<Test>(2, 5),
|
|
total_issuance_diff: 0,
|
|
expected_error: None,
|
|
},
|
|
TestCase {
|
|
description: "with enough existing dust",
|
|
from: ALICE_ADDR,
|
|
to: BOB_ADDR,
|
|
from_balance: BalanceWithDust::new_unchecked::<Test>(100, 10),
|
|
to_balance: BalanceWithDust::new_unchecked::<Test>(0, plank - 10),
|
|
amount: BalanceWithDust::new_unchecked::<Test>(1, 10),
|
|
expected_from_balance: BalanceWithDust::new_unchecked::<Test>(99, 0),
|
|
expected_to_balance: BalanceWithDust::new_unchecked::<Test>(2, 0),
|
|
total_issuance_diff: -1,
|
|
expected_error: None,
|
|
},
|
|
TestCase {
|
|
description: "receiver dust less than 1 plank",
|
|
from: ALICE_ADDR,
|
|
to: BOB_ADDR,
|
|
from_balance: BalanceWithDust::new_unchecked::<Test>(100, plank / 10),
|
|
to_balance: BalanceWithDust::new_unchecked::<Test>(0, plank / 2),
|
|
amount: BalanceWithDust::new_unchecked::<Test>(1, plank / 10 * 3),
|
|
expected_from_balance: BalanceWithDust::new_unchecked::<Test>(98, plank / 10 * 8),
|
|
expected_to_balance: BalanceWithDust::new_unchecked::<Test>(1, plank / 10 * 8),
|
|
total_issuance_diff: 1,
|
|
expected_error: None,
|
|
},
|
|
TestCase {
|
|
description: "insufficient balance",
|
|
from: ALICE_ADDR,
|
|
to: BOB_ADDR,
|
|
from_balance: BalanceWithDust::new_unchecked::<Test>(10, 0),
|
|
to_balance: BalanceWithDust::new_unchecked::<Test>(10, 0),
|
|
amount: BalanceWithDust::new_unchecked::<Test>(20, 0),
|
|
expected_from_balance: BalanceWithDust::new_unchecked::<Test>(10, 0),
|
|
expected_to_balance: BalanceWithDust::new_unchecked::<Test>(10, 0),
|
|
total_issuance_diff: 0,
|
|
expected_error: Some(Error::<Test>::TransferFailed.into()),
|
|
},
|
|
TestCase {
|
|
description: "from = to with insufficient balance",
|
|
from: ALICE_ADDR,
|
|
to: ALICE_ADDR,
|
|
from_balance: BalanceWithDust::new_unchecked::<Test>(10, 0),
|
|
to_balance: BalanceWithDust::new_unchecked::<Test>(10, 0),
|
|
amount: BalanceWithDust::new_unchecked::<Test>(20, 0),
|
|
expected_from_balance: BalanceWithDust::new_unchecked::<Test>(10, 0),
|
|
expected_to_balance: BalanceWithDust::new_unchecked::<Test>(10, 0),
|
|
total_issuance_diff: 0,
|
|
expected_error: Some(Error::<Test>::TransferFailed.into()),
|
|
},
|
|
TestCase {
|
|
description: "from = to with insufficient balance",
|
|
from: ALICE_ADDR,
|
|
to: ALICE_ADDR,
|
|
from_balance: BalanceWithDust::new_unchecked::<Test>(0, 10),
|
|
to_balance: BalanceWithDust::new_unchecked::<Test>(0, 10),
|
|
amount: BalanceWithDust::new_unchecked::<Test>(0, 20),
|
|
expected_from_balance: BalanceWithDust::new_unchecked::<Test>(0, 10),
|
|
expected_to_balance: BalanceWithDust::new_unchecked::<Test>(0, 10),
|
|
total_issuance_diff: 0,
|
|
expected_error: Some(Error::<Test>::TransferFailed.into()),
|
|
},
|
|
TestCase {
|
|
description: "from = to",
|
|
from: ALICE_ADDR,
|
|
to: ALICE_ADDR,
|
|
from_balance: BalanceWithDust::new_unchecked::<Test>(0, 10),
|
|
to_balance: BalanceWithDust::new_unchecked::<Test>(0, 10),
|
|
amount: BalanceWithDust::new_unchecked::<Test>(0, 5),
|
|
expected_from_balance: BalanceWithDust::new_unchecked::<Test>(0, 10),
|
|
expected_to_balance: BalanceWithDust::new_unchecked::<Test>(0, 10),
|
|
total_issuance_diff: 0,
|
|
expected_error: None,
|
|
},
|
|
];
|
|
|
|
for TestCase {
|
|
description,
|
|
from,
|
|
to,
|
|
from_balance,
|
|
to_balance,
|
|
amount,
|
|
expected_from_balance,
|
|
expected_to_balance,
|
|
total_issuance_diff,
|
|
expected_error,
|
|
} in test_cases.into_iter()
|
|
{
|
|
ExtBuilder::default().build().execute_with(|| {
|
|
set_balance_with_dust(&from, from_balance);
|
|
set_balance_with_dust(&to, to_balance);
|
|
|
|
let total_issuance = <Test as Config>::Currency::total_issuance();
|
|
let evm_value = Pezpallet::<Test>::convert_native_to_evm(amount);
|
|
|
|
let (value, dust) = amount.deconstruct();
|
|
assert_eq!(Pezpallet::<Test>::has_dust(evm_value), !dust.is_zero());
|
|
assert_eq!(Pezpallet::<Test>::has_balance(evm_value), !value.is_zero());
|
|
|
|
let result = builder::bare_call(to).evm_value(evm_value).build();
|
|
|
|
if let Some(expected_error) = expected_error {
|
|
assert_err!(result.result, expected_error);
|
|
} else {
|
|
assert_eq!(
|
|
result.result.unwrap(),
|
|
Default::default(),
|
|
"{description} tx failed"
|
|
);
|
|
}
|
|
|
|
assert_eq!(
|
|
Pezpallet::<Test>::evm_balance(&from),
|
|
Pezpallet::<Test>::convert_native_to_evm(expected_from_balance),
|
|
"{description}: invalid from balance"
|
|
);
|
|
|
|
assert_eq!(
|
|
Pezpallet::<Test>::evm_balance(&to),
|
|
Pezpallet::<Test>::convert_native_to_evm(expected_to_balance),
|
|
"{description}: invalid to balance"
|
|
);
|
|
|
|
assert_eq!(
|
|
total_issuance as i64 - total_issuance_diff,
|
|
<Test as Config>::Currency::total_issuance() as i64,
|
|
"{description}: total issuance should match"
|
|
);
|
|
});
|
|
}
|
|
}
|
|
}
|