I am working on an exercise involving liquid staking, where two different tokens are managed. It was suggested to me that I might try instantiating the Balances pallet twice, following the pattern of the full node instantiating the Collective pallet for both the Council and the TechnicalCommittee.
In testing, I am finding that the two instances are writing to the same location in storage, so that the balances of one token overwrite those of the other.
Example is here: https://github.com/psfblair/polkadot-academy-final-exam/commit/69a4cd5bc2caa6320a2e2034c59dcfa6a8802038
Details of testing:
Desired initial state:
user_account: Main: 10, Derivative: 70
pot_account: Main: 20, Derivative: 160
Additional state changes to be triggered during test:
user sends 3 units of main token to pot
pot sends 3 units of derivative token to user
Expected final state:
user_account: Main: 7, Derivative: 73
pot_account: Main: 23, Derivative: 157
Actual final state:
user_account: Main: 70, Derivative: 70
pot_account: Main: 160, Derivative: 160
When storage is initialized, the main balances are done first, and then the derivative balances. It appears that the derivative balances are overwriting the first (and then no transactions are made afterwards).
Here are some relevant excerpts from the code. From mock.rs, there are two instances of the balances pallet:
type MainToken = pallet_balances::Instance1;
impl pallet_balances::Config<MainToken> for Test {
type MaxLocks = frame_support::traits::ConstU32<1024>;
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = BalanceImpl;
type Event = Event;
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
type WeightInfo = ();
}
type DerivativeToken = pallet_balances::Instance2;
impl pallet_balances::Config<DerivativeToken> for Test {
type MaxLocks = frame_support::traits::ConstU32<1024>;
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = BalanceImpl;
type Event = Event;
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
type WeightInfo = ();
}
The runtime is constructed with the two instances:
frame_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
LiquidStakingModule: pallet_liquid_staking::{Pallet, Call, Storage, Event<T>},
MainBalances: pallet_balances::<Instance1>::{Pallet, Call, Storage, Config<T, Instance1>, Event<T, Instance1>},
DerivativeBalances: pallet_balances::<Instance2>::{Pallet, Call, Storage, Config<T, Instance2>, Event<T, Instance2>},
}
);
The pallet is configured to allow access to the two different currencies in which the balances are implemented:
impl crate::pallet::Config for Test {
...
type MainCurrency = MainBalances;
type DerivativeCurrency = DerivativeBalances;
}
MainCurrency
and DerivativeCurrency
are used by the pallet in lib.rs for e.g. making transfers.
Here is how mock.rs initializes the storage. It takes a vector of tuples of (accountId, mainBalance, derivativeBalance) and decomposes them into a vector of (accountId, mainBalance) tuples for the main balance storage, and a vector of (accountId, derivativeBalance) for the derivative balance storage:
pub fn new_test_ext(users: Vec<(u64, u128, u128)>) -> sp_io::TestExternalities {
let mut storage = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
GenesisConfig {
main_balances: MainBalancesConfig {
balances: users.iter().map(|(account_id, main_balance, _)| (*account_id, *main_balance) ).collect(),
},
derivative_balances: DerivativeBalancesConfig {
balances: users.iter().map(|(account_id, _, derivative_balances)| (*account_id, *derivative_balances) ).collect(),
},
..Default::default()
}
.assimilate_storage(&mut storage)
.unwrap();
let mut externalities = sp_io::TestExternalities::new(storage);
externalities.execute_with(|| System::set_block_number(1));
externalities
}
Is there something else I should be doing to make this work?