pendulum-chain/pendulum

Enable XCM assets on Pendulum

Closed this issue ยท 30 comments

Context

In order to move an asset from a parachain to Pendulum, the asset needs to be enabled, and opening an HRMP channel wouldn't be enough.
e.g. Enabling USDT on Pendulum from Asset Hub

Requirement

Based on the recent HRMP channels that were opened, and some of the upcoming ones, purpose of this ticket is to enable the following assets on Pendulum:

Asset Origination Asset ID Decimals Source
USDC Asset Hub 1337 6 subscan
EQD Equilibrium 6,648,164 9 Telegram
PDEX Polkadex 12 subscan
EURC Pendulum
BRZ Moonbeam 18 subscan
  • USDC
  • EQD
  • PDEX
  • EURC
  • BRZ

@annatekl Can you specify what EQ and EQD are? It looks to me like Equilibrium has only the native asset and nothing more. In this comment you write that EQ seems to be the native token and EQD its native decentralized stablecoin but I have problems to find where on the runtime the latter coin resides (stored and managed).

I did checked there whitepaper and what I can see is:

EQ tokens are stored in user account balances within the Equilibrium blockchain's storage system.
Users can query their EQ balances using storage queries compliant with Polkadot.JS storage interfaces

https://www.npmjs.com/package/@equilab/api
Query storage
Storage queries are compliant with Polkadot.JS storage interfaces.

Get balances from storage method is using currencyFromU64 to decode asset u64 id into token name (eg 'eq')

import { currencyFromU64, u64FromCurrency } from "@equilab/api/equilibrium";

function getBalances(api: Api) {
return async function (account: string): Promise<{
ok: boolean;
lock?: string;
balances?: Map<string, string>;
}> {
const accountInfo = await api._api.query.system.account(account);
if (!accountInfo.data.isV0) return { ok: false };

const lock = accountInfo.data.asV0.lock.toString(10);

const balances = accountInfo.data.asV0.balance
  .toArray()
  .reduce(
    (acc, [id, balance]) =>
      acc.set(
        currencyFromU64(id),
        balance.isPositive
          ? balance.asPositive.toString(10)
          : `-${balance.asNegative.toString(10)}`,
      ),
    new Map<string, string>(),
  );

return { ok: true, lock, balances };

};
}
Successfull output looks like this:

{ ok: true, lock: '22000000000', balances: { 'eq' => '72000000000', 'dot' => '597056440465' } }
Balance has 1e9 decimals places and thus '72000000000' equals 72 eq.

the documentation doesn't explicitly detail the runtime storage of EQD, but seems like that EQD would be managed using mechanisms similar to those mentioned for EQ tokens

confirmed by Equilibrium:
We have Assets pallet which is a storage of asset parameters.

  • All of our assets are 9 decimals
  • Balances are stored in a system pallet.

Asset Ids may be fetched using polkadot.js and the following script (Developer -> Javascript)

function hex2a(hexx) {
var hex = hexx.toString();
var str = '';
for (var i = 0; i < hex.length; i += 2)
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
return str;
}

const assets = (await api.query.eqAssets.assets()).toJSON();

for (const asset of assets) {
const assetSymbol = hex2a(asset.id.toString(16)).padStart(5, ' ');
const assetId = asset.id.toString(10).padStart(13, ' ');
console.log(Asset: ${assetSymbol} assetId:${assetId});
}

Okay, thank you, understood.

Two things to remark:

  • It seems like their parachain is closed source โ€“ at least their open source repo is very out of date (almost three years old) and does not contain this asset pallet yet. Can you ask where to find the open source repository of their chain, please @annatekl? This is crucial particularly so that we can see the index of their asset pallet.
  • This assets pallet is not the standard asset pallet but a pallet that they implemented, it is actually called eqAssets in their runtime.

Using their definition in the previous comment I can conclude that the assets ids are as follows:

  • EQ: asset id is 25,969
  • EQD: asset id is 6,648,164

Hence, I assume that these assets are defines as the following xcm multilocations:

MultiLocation {
  parents: 1,
  interior:
    X3(
      Parachain(<equilibrium paraId>),
      PalletInstance(<index of eqAssets>),
      GeneralIndex(<25969 or 6648164>),
    ),
  }

@annatekl as mentioned by @TorstenStueber , let's consider adding tokens from parachains that we want to integrate with after Equilibrium too.

@vadaynujra I guess that you specifically refer to Polkadex?

@TorstenStueber this is correct:

EQ: asset id is 25,969
EQD: asset id is 6,648,164

Repo link

Oh thanks for the link. This is really strange: the official Equilibrium website only links to this GitHub, which is a completely different GitHub organization and only contains their outdated parachain code.

Could be helpful to give them this hint so that they can change it.

Consider adding:

  1. USDC
  2. EQD (already covered)
  3. PDEX
  4. EURC (natively issued)

@ebma I have added the details for the assets (EURC is going to be issued on Pendulum), PDEX is a native token on polkadex so not sure about the id and it was not on the subscan, the same for BRZ - no details

b-yap commented

equilibrium asset info found in asset.rs

pallet-asset instance id found in lib.rs

para id:
Screen Shot 2023-11-08 at 8 35 21 PM

ebma commented

For the BRZ token, the multilocation should be the following:

{
  parents: 1,
  interior: {
    X3: [
      { 
        Parachain: 2004
      },
      {
        PalletInstance: 110
      },
      {
        AccountKey20: {
          key: '0xD65A1872f2E2E26092A443CB86bb5d8572027E6E'
        }
      }
    ]
  }
}

This is documented here, by inserting the address of this smart contract into the respective field.

b-yap commented

need Polkadex.

@b-yap do you need asset id of polkadex?

b-yap commented

@annatekl yes the assetId of PDEX, or similar to what Marcel shared; which is as AccountKey20.

Though I think the pallet instance is 25 but please correct me if I'm wrong.

ebma commented

@b-yap I found that they use a crate called xcm-helper which defines the conversion functions e.g. here. But I'm still not sure what the MultiLocation of their native asset would look like.
@pendulum-chain/product if we have some communication channel with them, can you please ask Polkadex what the MultiLocation of their native asset looks like?

@b-yap @ebma I asked via telegram, they provided me with Multilocation: {parents: 1, interior: {x1:{parachain: 2040}}}

b-yap commented

for EURC, what are the ff. tickets that need to be resolved first?
https://github.com/pendulum-chain/tasks/issues/87
https://github.com/pendulum-chain/tasks/issues/128
#310
#331
#334

^ I'm naming a few, but I'm not quite sure
@annatekl

ebma commented

The only real blocker I see is #342. Once that is merged, you should be able to configure EURC. Though we did not yet specify how we want the MultiLocation of our new CurrencyId::Token types to look like externally. We would also need to discuss this in the tech team. Do you have a suggestion for the MultiLocation @b-yap?

b-yap commented

@ebma I checked all the variants in Junction, and what makes sense (to me) is the GeneralIndex.
We could follow how Equilibrium does it, with Asset(<value>) where <value> came from fn from_bytes()

So it will be

MultiLocation {
	parents: 1,
	interior: X3(
		Parachain(id),
		PalletInstance(10),
		GeneralIndex(from_bytes(b"EURC"))
	),
}

and

MultiLocation {
	parents: 0,
	interior: X2(
		PalletInstance(10),
		GeneralIndex(from_bytes(b"EURC"))
	),
}

Ah but with Token(u64), Will EURC be Token(0) ? Token(1) ?

ebma commented

Hmm interesting. I would put EURC as Token(0) but we can use anything.
For the MultiLocation, I'm not sure. First of all, we should probably change the number in PalletInstance to point to the Tokens pallet as 10 points to our Balances pallet which only makes sense for our native token.

Now that we point to our Tokens pallet, we still need to map the asset somehow to our CurrencyId enum. We should probably already think about how we can map any type of our CurrencyId enum to a MultiLocation so we should also consider Stellar-type assets in addition to the CurrencyId::Token().

We could use a combination of GeneralIndex and GeneralKey with the index pointing to the index of the type in our CurrencyId enum, with Stellar being index 2 and Token being index 4, and then putting the actual specifier into the GeneralKey.

WDYT @b-yap and also @pendulum-chain/devs?

I think the solution of using the junction GeneralIndex and GeneralKey would work very well. Because even if we later would like to use GeneralIndex for other MultiLocations that do not fall into the category of the CurrencyId enum, we still have a very large space (u128) to represent them.
I imagine that for Stellar assets the key would be something like GeneralKey([from_bytes(b"USDT", b"Issuer..."].concat()) right?

b-yap commented

@gianfra-t The GeneralKey has a max data length of 32. b"USDT" + b"Issuer..." might be difficult.
Is it possible to have multiple GeneralKey junctions to exist? Although usage of the GeneralKey as pointed in the comment, must be avoided (if possible).

@ebma I'm using the mod latest, so this is I think the link of GeneralKey.
Is there a preference to use v2 rather than v3 immediately?

MultiLocation {
	parents: 0,
	interior: X3(
		PalletInstance(53), // 53 is Token pallet
		GeneralIndex(4),
		GeneralKey{ ?? }
	),
}

Let's sayEURC is Token(0); will the GeneralKey represent the "0" ? It makes sense for it to be 0.

ebma commented

Is it possible to have multiple GeneralKey junctions to exist?

Yes we can use those multiple times.

Although usage of the GeneralKey as pointed in the comment, must be avoided (if possible).

I don't think we need to avoid it, the comment is probably meant to make people try using more specific items if possible. But there is no problem if we don't.

Is there a preference to use v2 rather than v3 immediately?

Not really, I just chose the first that I found ๐Ÿ˜…

Let's say EURC is Token(0); will the GeneralKey represent the "0" ? It makes sense for it to be 0.

Yeah we could do that.

The GeneralKey has a max data length of 32. b"USDT" + b"Issuer..." might be difficult.

So for Stellar assets we could also use two consecutive GeneralKeys like

interior: X4(
		PalletInstance(53), // 53 is Token pallet
		GeneralIndex(2),
		GeneralKey{ length: 4, // or 12 if it is an AlphaNum12
                                       data: code  
                 },
                 GeneralKey{ length: 32,
                                       data: issuer  
                 },

	),

Here are a few unrelated thoughts.

  1. @b-yap pointed out that using the value Token(0) to stand for EURC is not elegant. I agree and this was actually my misguiding input. However, I assume that this now already rolled out on Amplitude (?) and therefore too hard to change again (?)

  2. Whether we use V2 or V3 should not matter. Usually V3 is just an extension of V2 and everything we an define as V2 would automatically internally converted to V3 if applicable. This conversion is transparent and we don't need to care about this (take this advice with care).

  3. I agree with Marcel's previous comment. I think it's better to be explicit and therefore prefer to use multiple GeneralIndex and GeneralKey combinations.

b-yap commented

@TorstenStueber @ebma

However, I assume that this now already rolled out on Amplitude (?) and therefore too hard to change again (?)

-> #342 merged just recently.

Oh, merged so recently that it is not part of the Amplitude release yet so that we could still change this.

ebma commented

I would propose the following. Considering our current CurrencyId enum

pub enum CurrencyId {
	Native = 0_u8,
	XCM(u8),
	Stellar(Asset),
	ZenlinkLPToken(u8, u8, u8, u8),
	Token(u64),
}

Mappings

Native

We actually define a different MultiLocation for our Native asset, referring to the Balances pallet.

MultiLocation {
	parents: 0,
	interior: X1(
		PalletInstance(10),
	),
}

XCM

As these are all foreign assets, their MultiLocation is defined by the source chain.

Stellar

For StellarNative we might just leave out

MultiLocation {
	parents: 0,
         interior: X2(
		PalletInstance(53), // 53 is Token pallet
		GeneralIndex(2),
	),
}

For Stellar::AlphaNum4

MultiLocation {
	parents: 0,
        interior: X4(
		PalletInstance(53), // 53 is Token pallet
		GeneralIndex(2),
		GeneralKey{ length: 4,  data: code }, // eg. b"USDC"
                 GeneralKey{ length: 32, data: issuer  }, // the raw binary of the public key
	),

For Stellar::AlphaNum12

MultiLocation {
	parents: 0,
        interior: X4(
		PalletInstance(53), // 53 is Token pallet
		GeneralIndex(2),
		GeneralKey{ length: 12, data: code  }, // eg b"USDC\0\0\0\0\0\0\0\0"
                 GeneralKey{ length: 32, data: issuer },
	),

ZenlinkLPToken

We need to encode the four u8s

MultiLocation {
	parents: 0,
        interior: X4(
		PalletInstance(53), // 53 is Token pallet
		GeneralIndex(3),
		GeneralKey{ length: 4, data: [u8, u8, u8, u8]  }, // We can just concatenate them
	),

Token

MultiLocation {
	parents: 0,
        interior: X4(
		PalletInstance(53), // 53 is Token pallet
		GeneralIndex(4),
		GeneralIndex(index), // The index of our token in the token enum
	),
}

@ebma Great, I like it.