pendulum-chain / pendulum

GNU General Public License v3.0
43 stars 15 forks source link

Enable XCM assets on Pendulum #296

Closed annatekl closed 9 months ago

annatekl commented 1 year ago

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
TorstenStueber commented 1 year ago

@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).

annatekl commented 1 year ago

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

annatekl commented 1 year ago

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

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}); }

TorstenStueber commented 1 year ago

Okay, thank you, understood.

Two things to remark:

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

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>),
    ),
  }
vadaynujra commented 1 year ago

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

TorstenStueber commented 1 year ago

@vadaynujra I guess that you specifically refer to Polkadex?

annatekl commented 1 year ago

@TorstenStueber this is correct:

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

Repo link

TorstenStueber commented 1 year ago

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.

annatekl commented 1 year ago

Hey team! Please add your planning poker estimate with Zenhub @adelarja @ashneverdawn @b-yap @ebma @TorstenStueber

vadaynujra commented 11 months ago

Consider adding:

  1. USDC
  2. EQD (already covered)
  3. PDEX
  4. EURC (natively issued)
annatekl commented 10 months ago

@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 10 months ago

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 10 months ago

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 10 months ago

need Polkadex.

annatekl commented 10 months ago

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

b-yap commented 10 months ago

@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 10 months ago

@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?

annatekl commented 10 months ago

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

b-yap commented 10 months ago

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 https://github.com/pendulum-chain/pendulum/issues/310 https://github.com/pendulum-chain/pendulum/issues/331 https://github.com/pendulum-chain/pendulum/issues/334

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

ebma commented 10 months ago

The only real blocker I see is https://github.com/pendulum-chain/pendulum/pull/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 10 months ago

@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 10 months ago

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?

gianfra-t commented 10 months ago

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 10 months ago

@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 10 months ago

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  
                 },

    ),
TorstenStueber commented 10 months ago

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 10 months ago

@TorstenStueber @ebma

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

-> https://github.com/pendulum-chain/pendulum/pull/342 merged just recently.

TorstenStueber commented 10 months ago

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

ebma commented 10 months ago

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
    ),
}
TorstenStueber commented 10 months ago

@ebma Great, I like it.