While I agree with @rphmeier about what he wrote in Meta: Convention Creation over Standardization, I think fungible tokens are a well enough understood domain to define a standard for (at least try to and for some parts of it).
General Considerations
The following first talks a little What is important to consider for a standard in a multi-chain environment.
Firstly, there exist 3 perspectives in this world,
- Intra-Chain: Components of the same chain interacting with each other
- Inter-Chain: XCM in our world here
- Exter-Chain: External world interacting with a chain
where the Inter- and Exter-Chain perspective can be the same sometimes. Accepting this, it makes the most sense to define standards for each perspective separately but not independently of each other.
Secondly, a standard in this world MUST NEVER rely on the structure of the state.
Ethereum standards like ERC-20 & ERC-721 are so successful because they define interfaces and leave it up to the implementor how to structure the state of its contract. As long as your contract adheres to the interface, it can contain additional functionality and arbitrary state, while still being compatible with it.
In Substrate this is currently not the case. In Substrate, pallets are the defacto implicit definition of conventions. Taking pallet-balances
as an example, all external integrations with a chain rely on the storage of TotalIssuance
to not change its location in order to query the issuance of the native token of a chain. I would argue that at this point it is almost impossible to refactor pallet_balances::AccountData
or pallet_balances::TotalIssuance
.
Having the above in mind,
Standardizing Exter-APIs
Substrate already provides the âinterface-likeâ nature of Ethereum contracts for querying the state of a chain in the form of runtime APIs.These APIs are especially useful for off-chain computations or light-client integrations.
- Retrieving information MUST be based on runtime-APIs
- Retrieving information MUST NOT define/rely on specific storage locations or structures
Standardizing Inter-APIs
If we agree on developing standards not based on pallets but rather on interfaces, standardizing internal APIs is harder. While the ecosystem already uses common traits like fungibles::*
it would be extremely useful for tokens to have a standard for submitting extrinsics for transferring tokens. Pallet-Balances is the defacto standard for native tokens, Orml-Tokens
the standard for other tokens.
In order to standardize an API from an extrinsic perspective it is needed that
- The Pallet has the same âinvariant indexâ across chains in the chains runtime call enum
E.g.:OrmlTokens: orml_tokens::{Pallet, Storage, Event<T>, Config<T>} = 77
- The Palletâs calls have the same signature and order
With this, it would be possible to submit the same scale-encoded bytes for a call (Omitting differences in the signing bytes) on different chains. Would be great if we could standardize retrieving the SignedExtra
via a runtime-API (I know metadata contains it already but it is quite harder to deserialize than just getting a blob back to also sign).
Standardizing Intra-APIs
The Intra-Chain perspective is not part of this proposal as runtime-APIs currently are not callable via XCM and XCM already provides a detailed token transferring standard that allows for compatibility.
Proposed Standard
-
Common runtime-API
decl_runtime_apis! { pub trait Tokens { fn symbols() -> Vec<BoundedVec<u8, 32>>; fn name(symbol: BoundedVec<u8, 32>) -> BoundedVec<u8, 128>; fn decimals(symbol: BoundedVec<u8, 32>) -> u32; fn total_issuance(symbol: BoundedVec<u8, 32>) -> u128; fn balance_of(symbol: BoundedVec<u8, 32>, who: [u8; 32]) -> u128 } }
-
Common pallet at index x in the Call enum.
trait Transfer<AccountId> { type Balance; type CurrencyId; fn do_transfer(who: AccountId, currency: Self::CurrencyId, amount: Self::Balance) -> DispatchResult; } trait Config: frame_system::Config { type Balance: TryFrom<u128>; type CurrencyId: TryFrom<BoundedVec<u8, 32>>; type HandleTransfer: Transfer<AccountId, Balance = Balance, CurrencyId = CurrencyId>; } #[pallet::Call] impl<T: Config> Pallet<T> { fn transfer(origin: OriginFor<T>, currency_id: BoundedVec<u8, 32>, amount: u128) -> DispatchResult { let who = ensure_signed(origin)?; let amount = amount.try_into().ok_or(Error::<T>::BalanceConversionFailed)?; let currency_id = currency_id.try_into().ok_or(Error::<T>::CurrencyIdConversionFailed)?; Pallet::<T>::do_transfer(who, amount, currency_id) } }
The currencies will be provided by their symbol
, as those abstract over the specific CurrencyId
enums of the respective chains.
I left out the allowance
part of ERC-20 intentionally. Although it would be really beneficial to have, I guess it is also quite a big security risk.
Anyways, I hope we can discuss a bit here if this makes sense and I am happy to get some feedback on the idea.