🪐 Saturn XCM Multisig Integration on Kusama: A Technical Discussion

This forum post aims to open a discussion to get community and core Polkadot developer sentiment towards integrating the XCM configs for Tinkernet’s Saturn multisig solution into the Kusama runtime.

For an overall explanation of what Saturn is and accomplishes, check out this article.

Saturn is a non-custodial solution for bridgeless multichain asset management that is designed to provide all of its users, whether they be individuals, communities, organizations, or institutions, with the ability to navigate across all of web3 seamlessly, as a single unit, and without barriers.

Getting down to the technical level, the backend infrastructure of Saturn is mainly composed of 3 parts, those being:

  • INV4 Pallet, which provides the functionality for creating, managing and executing transactions through multisigs.
  • Rings Pallet, which acts as an abstraction over XCM locations, assets and operations for INV4 managed multisigs.
  • Saturn XCM configs, these are a set of configs much like those found in the xcm-builder crate and are specifically written to accept and derive accounts exclusively for Saturn multisig origins coming from the Tinkernet parachain.

Let’s go over the general flow of a Saturn multisig creation and execution:

  • Multisig created with integer ID 0
  • Account derived with hardcoded salt + integer 0 as 0x12345
  • Multisig now transacts within Tinkernet as a custom origin that carries the integer ID 0
  • When the multisig calls extrinsics that require a signed origin, the account derivation automatically applies and converts the origin to Signed(0x12345)
  • When the multisig calls extrinsics made to manage itself or those of the Rings Pallet, it remains as the custom origin with it’s ID 0

Why do we need specific configurations to be added to the receiver chain’s runtime?

The short answer is UX! By using these configs we can apply to the receiver chain the same exact derivation function that generated accounts for those multisigs within Tinkernet, which means that multisig 0 with derived account in Tinkernet 0x12345 can maintain the same account in Kusama and any other parachain that also includes these configs! This is the key to achieving a seemless experience across the paraverse using a single multisig deployment.

XCM security considerations
While the above explains why and how we approach the issue of mapping 0x12345 from the sender chain to 0x12345 in the receiver chain instead of 0x12345hash(0x12345), it doesn’t explain why we can’t simply use the AccountId32 junction and expect that the receiver chain takes the account from that junction and maps to that same account within their runtime.

The reason why the simpler approach mentioned above is not used is because that introduces a security concern that stems from the receiver chain having to trust the sender chain. Imagine the receiver chain does accept AccountId32(0x12345) and maps it to 0x12345, the receiver would then have to trust that the sender runtime does not allow for an origin like AccountId32(receiver_treasury_account) to be sent, and with frequent runtime upgrades this quickly becomes a very difficult and high maintenance task.

To conclude on the security considerations, I would also like to mention that, in the future, the simpler but unsafe approach described above could be used under a SPREE module, as that would allow the receiver to programatically verify that, even after a runtime upgrade, the relevant module hasn’t changed.

But until SPREE becomes available and that approach becomes possible, this is the best solution for checking all the UX requirements while keeping the security considerations in mind.

Now let’s go over the configs and how they’re implemented:

Saturn multisigs have the following MultiLocation pattern

MultiLocation {
    parents: _, // ancestry depends on the point of reference. 0 if from parent, 1 if from sibling.
    interior: Junctions::X3(
        Junction::Parachain(2125), // Tinkernet ParaId in Kusama.
        Junction::PalletInstance(71), // Pallet INV4, from which the multisigs originate.
        Junction::GeneralIndex(_) // Index from which the multisig account is derived.
    )
}

So implementing them in the runtime is fairly simple, we only need 3 things, a barrier, a location to AccountId converter and a location to origin converter. For Kusama we already have the WithComputedOrigin<AllowTopLevelPaidExecutionFrom<Everything>, ...> barrier, which ticks the requirements for Saturn, so all that remains are the converters.

For the converters the approach is fairly simple too, let’s check the code for both of them:

pub fn derive_tinkernet_multisig(index: u32) -> [u8; 32] {
    (
        H256([
            212, 46, 150, 6, 169, 149, 223, 228, 51, 220, 121, 85, 220, 42, 112, 244, 149, 243, 80,
            243, 115, 218, 162, 0, 9, 138, 232, 68, 55, 129, 106, 210,
        ]),
        index,
    )
        .using_encoded(sp_io::hashing::blake2_256)
}

pub struct TinkernetMultisigAsAccountId<AccountId>(
    PhantomData<(AccountId)>,
);
impl<AccountId: Clone>
    Convert<MultiLocation, AccountId>
    for TinkernetMultisigAsAccountId<AccountId>
{
    fn convert(location: MultiLocation) -> Result<AccountId, MultiLocation> {
        let id = match location.clone() {
            MultiLocation {
                parents: _,
                interior:
                    Junctions::X3(
                        Junction::Parachain(para_id),
                        Junction::PalletInstance(pallet_index),
                        Junction::GeneralIndex(id),
                    ),
            } => AccountId::decode(&mut TrailingZeroInput::new(&derive_tinkernet_multisig(
                        id as u32,
                   )))
                   .expect("infinite length input; no invalid inputs for type; qed"),
            _ => return Err(location),
        };
        Ok(id)
    }
}


pub struct DeriveOriginFromTinkernetMultisig<Origin>(
    PhantomData<(Origin)>,
);
impl<Origin: OriginTrait>
    ConvertOrigin<Origin> for DeriveOriginFromTinkernetMultisig<Origin>
{
    fn convert_origin(
        origin: impl Into<MultiLocation>,
        kind: OriginKind,
    ) -> Result<Origin, MultiLocation> {
        let origin = origin.into();
        match (kind, origin.clone()) {
            (
                OriginKind::Native,
                MultiLocation {
                    parents: _,
                    interior:
                        Junctions::X3(
                            Junction::Parachain(para_id),
                            Junction::PalletInstance(pallet_index),
                            Junction::GeneralIndex(id),
                        ),
                },
            ) => Origin::signed(
                       Origin::AccountId::decode(&mut TrailingZeroInput::new(&derive_tinkernet_multisig(
                           id as u32,
                       )))
                       .expect("infinite length input; no invalid inputs for type; qed"),
                   ),
            (_, origin) => Err(origin),
        }
    }
}

To explain the above:

derive_tinkernet_multisig is the account derivation function, it has the hardcoded hash already in it and takes integer IDs as the dynamic part of the account derivation.

TinkernetMultisigAsAccountId is the MultiLocation to AccountId converter config.

DeriveOriginFromTinkernetMultisig is the MultiLocation to Origin converter config.

We do have these configs in a library that can be easily adopted without having to add the structs and their impls in the runtime, however since Kusama is the frontrunner of Substrate/Polkadot releases and neither support proper semver releases yet, I propose that they be implemented manually instead of using the library.

Unlocking a multichain multisig:

By integrating the XCM configs for Tinkernet’s Saturn multisig solution into the Kusama runtime, a truly multichain experience can be provided to users & institutions, an experience we believe has the potential to become the best in the entire industry. This will help to onboard more users - individuals & institutions - to the Kusama (and soon, Polkadot) ecosystem.

Current leading multisig solutions today, such as Gnosis Safe, are not just limited to a single ecosystem, but they limit their deployments to a single chain. While this is far from the best solution, it has primarily been the only solution available to individuals & institutions, which has helped Gnosis Safe alone to manage over $100 billion in digital assets.

With the Saturn Multisig providing not just a more featureful experience for individuals & institutions, but also providing a more seamless & fluid experience complemented by the ability to operate across all blockchains as a single multisig entity, the core team behind the Saturn Multisig confidently believe that Saturn will one day be used to manage over $1 trillion dollars across numerous blockchain networks.

This integration will ultimately conclude the delivery of the Saturn Multisig & Saturn SDK, which was funded by the Kusama Network treasury under Referendum #18 of Kusama OpenGov.

14 Likes

The PR for this has been opened at: Introduce Tinkernet multisig XCM configs by arrudagates · Pull Request #7165 · paritytech/polkadot · GitHub

2 Likes