4

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?

1 Answer 1

3

I believe you need to adjust the AccountStore type in the second balances pallet instance to store the balances in the Balances pallet storage.

See the docs here: https://github.com/paritytech/substrate/blob/master/frame/balances/src/lib.rs#L495

impl pallet_balances::Config for Runtime {
    type AccountStore = StorageMapShim<Self::Account<Runtime>, frame_system::Provider<Runtime>, AccountId, Self::AccountData<Balance>>
}

Something like that.

1
  • 1
    Thank you ! For the specific implementation, I have it working with: type AccountStore = StorageMapShim<pallet_balances::pallet::Account<Test, DerivativeToken>, frame_system::Provider<Test>, Self::AccountId, pallet_balances::AccountData<BalanceImpl>>;
    – psfblair
    Aug 1, 2022 at 10:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.