ICRC-1 multiple token ledgers inside one canister

There are a few reasons to allow one canister to have multiple token ledgers.
Not sure if it was discussed in the WG, but I suppose having it in the forum as well will be ok.

Advanced DeFi could use the speed, security & simplicity of this feature.

Scalability could be solved with something like that ( high minimum transfer amount if it results in the creation of a new account unless the whole balance gets transferred )

I’ve personally come to a use case that will benefit this.

@memecake Sonic has multiple LPs inside one canister. If they don’t change their whole architecture, they won’t be able to expose the LP tokens with ICRC-1. Currently, they use their custom interface to provide that functionality.

@Maxfinity I assume the Ethereum EVM will also have a lot of tokens inside one canister and they will want to expose them to the IC too.

@mariop The ledger standard can add this feature inside or perhaps a new magical feature that allows one canister to have multiple principals each with a different interface will do it. Are there any plans for such a feature?

1 Like

We are moving towards multi canister architecture.


I can see one way to do it for ICRC-1 and ICRC-2: you can add an optional parameter that defaults to one of the tokens to each method. The only problem with this is that all integrations would either have to know this or ignore it and work only with the default token.

We never discussed about supporting multiple tokens in the same canister in the context of token standards because on the IC it makes sense to have one canister per token. You talked about speed, security and simplicity but the reverse is true if you mix code for different tokens in a single canister.


Let me try to provide an example where that may be useful.
In a liquidity pool between ICP and XTC.
Users send ICP and XTC to a canister. Then they call a function that notifies them and also converts the ICP and XTC into a LP token. In practice, you have 3 tokens inside one canister.
Wrapped ICP, wrapped XTC, and the LP token.
You can expose the LP token with icrc1 ok, but then the other two tokens will also need balance & transfer functions, so you will create custom ones like LPs do today. (to withdraw your funds)
Instead of that, the canister could be considered to have 3 tokens where each has a ledger interface and there are additional functions to notify & convert.
This LP example is the most basic one possible. There can be other DeFi contracts that work with a dozen of tokens - perhaps a trading bot. Perhaps a pool that adds a locked LP token or has a stablecoin LP between 3 tokens.
Not sure if that’s the way to go, just speculating. If the ledger standard won’t allow it and is only meant for ‘source’ ledgers, then there should probably be another to cover the needs of ‘proxy’ ledgers.

Probably an unconventional way to think about it. The simplest DeFi canister has minimum 3 tokens and they all belong to the user, but it can only provide a standard interface for one of them while all 3 need balance, fee & transfer. But we will differentiate them and only one token will use icrc1 and the other will use something else.


I’m not following. The fact that the user can send different tokens to a canister and notify the canister about the transaction doesn’t imply that said canister must have the interface for the tokens. For instance, you can send ICPs to a liquidity pool by using the ICP Ledger. You don’t need wrapped ICP.

Yes, I probably shouldn’t have used ‘wrapped’. The ICP and the XTC are in the custody of the canister.
At that point, you will either add them to the liquidity pool, remove them from it or transfer them out. The canister has to relay your transfer call to their ledgers.

Here is some code if it could handle multiple tokens inside one canister. Then besides ‘add_liqudity’ and ‘stake’ everything else is just token movement covered by the standard.
The following code places two tokens inside an LP and then locks them in another canister.

await icpledger.icrc1_transfer({ to=my_pool_account, .. });
await xtcledger.icrc1_transfer({ to=my_pool_account, ... });

await pool.icrc1_balances_of(my_pool_account)
// [("XTC", 100000) , ("ICP", 200000), ("LP_XTC_ICP", 0)

await pool.add_liquidity(...) // custom contract functions

await pool.icrc1_balances_of(my_pool_account)
// [("XTC", 60000) , ("ICP", 0), ("LP_XTC_ICP", 40000)

await pool.icrc1_transfer({ to=my_account, token="XTC", amount=60000, ... })

await pool.icrc1_transfer({ to=my_mining_account, token="LP_XTC_ICP", amount=40000, ... })

await mining.stake(...)
await mining.icrc1_balances_of(my_staking_account)
//  [ ("REWARD", 100000), ("LP_XTC_ICP", 20000),  ("LOCKED_LP_XTC_ICP",60000), ("LP_XTC_BTC", 0),  ("LOCKED_LP_XTC_BTC",60000), ...

I see. Then I think just adding the token field to the arguments as you have done is enough. We could make a standard that extends icrc-1 eventually.