With the Sodium launch happening soon, I thought it’d be a good time to start thinking about token standards.
Here’s my proposal for a unified canister interface, with support for single/multiple tokens per canister, fungible and non-fungible tokens. Feedback wanted!
We should probably come up with a process for proposals (tentatively calling them ICIPs (Internet Computer Improvement Proposals). Perhaps a public github repo under dfinity?
In terms of operator permissions I was thinking they should be more granular?
From the top of my head even though an operator has access to just one token type maybe the principal should have the option to set a hard expense limit ? like 10% (or a fixed nuber) of the total bucket?
Eg: I want my canister to be free to a certain point (trial period).
A simple way to do this is to set token permissions for each operator:
type TokenAllowance = {
tokenId: TokenId;
allowance: ?Nat; // if null, unlimited allowance for this TokenId
}
type TokenPermission = {
#All;
#Some: [TokenAllowance];
};
type OperatorAction = {
#SetPermissions: TokenPermission;
#RemoveOperator;
};
type OperatorRequest = {
owner: User;
operators: [(User, OperatorAction)]
};
Another approach is to handle permissions in a separate canister (see EIP-1761 scoped approvals). This TokenPermissions canister could dynamically update allowances, eg. 10% of total $ value per day. Operators would then call TokenPermissions.transfer(), and if approved, the transfer would be proxied to the token canister. This approach gives users a central place to manage permissions, but requires operators to know about this second canister and also adds another async call.
Thanks for the creation of this. Are you thinking of a token canister of being static after being set up? By that i mean that the interface can’t change? If not it might be useful to have some space for upgrades / new implementations.
I think the core functions (eg. getBalance, transfer) should never change. Any additional functions that devs want to add are non-standard, and should be documented properly.
There is a potential issue of adding non-standard functionality to the transfer function; some rebase tokens update their supply on every transfer call, which could lead to weird behavior. Consumers need to be aware of this when integrating tokens, but I don’t have a good solution to this…
Also, we have experimental token code in motoko base. Looks like Nat is used for balances: if 1e12 cycle = 1 CHF, then the smallest unit of 1 cycle = 1e-12 CHF
@crusso Has the team thought about a unified API for system funds and user tokens? On Ethereum, system ETH is handled completely different from userspace erc20 tokens, creating the need for a wrapped ETH. It would be ideal if we didn’t need this
And what about a running canister that already has thousands of users. Imagine I want to add additional functionality to it. I know that there’s a Motoko keyword to ensure either stability or flexibility, although that is only for variables. From the talks I imagined it so far that you can create a token canister following your proposal, having the same interface and implementing the proposed functionality in a way that satisfies the constraints.
As a user of that canister I wouldn’t want anyone to have access to the canister in a way that changes it’s functionality or state. I want to see the token canister deployed, be able to review it’s code and be sure that it will stay like this forever.
So if there has to be some new functionality added to the token canister, would I always need to go through the NNS to do so? What if the token canister hasn’t got enough users and that way my proposal to change won’t even be voted on because not enough people are interested in it. What can I as a owner/maintainer do to change it? I could if course create new one with the needed functionality and transfer the state to it. But this new one needs to be again accepted by the users.
So my point was if it would be possible to add some default extensibility – like extra bits in protocol headers – to not have to go through all that. And I would be happy to hear your thoughts on how the above mentionend process would look like, maybe I’m completely off track.
Right, how can we ensure that a token canister doesn’t upgrade maliciously? We don’t have a keyword to force canisters to not be upgradeable (at least not yet). It seems like there’s two options for token issuers:
No upgrade - Set the owner of your canister to a known unused address (eg. 0x0000…), allowing your users to verify that no upgrade is possible.
Upgrade - Retain the ability to upgrade at any time, but you must convince your users that you won’t steal their funds, and be transparent about all upgrades. If you’re introducing breaking changes, it’s up to you to notify all consumers. If your token is a key dependency of many other projects, using a formal governance process is probably a good idea.
Option 1 - token ledger
Define a new ledger interface and include that in this token interface. Token creators choose pricing, eg. all users can query their latest 100 transfers for free, but need to pay for more access.
Pros - more flexibility for token creators
Cons - could cause lack of interoperability, hard to aggregate data
Option 2 - centralized ledger
An independent party creates and maintains a token ledger service, which acts as the central location to store all token transactions.
Pros - easy for token creators, easy to aggregate data, can be most secure
Cons - highest cost, prone to corruption
Option 3 - user ledger
All users maintain their own token ledgers (in addition to their own wallet canister) and pay for all reads and writes.
Pros - data sovereignty, privacy, lowest cost
Cons - complexity (users need to specify their ledger canister when interacting with a token), hard to aggregate data
Perhaps a better idea is a combination of 1 and 3 - token creators that need to store their own history for compliance purposes should do so however they want, but also integrate with the “standard user ledger” if provided.
I do notice that I doubled up on a change that you already mentioned in this forum (adding an allowance to operators). Our approaches are almost identical, so happy to revert to what you suggested above.
I am currently working on a number of live tokens (NFTs and FTs) and I found my core calls were very similar and I have updated my code to the standard that you have proposed. I think this is a really good standard and gives us a good base to develop from.
I have also tried to get around the idea of having a wrapped ICP, and initially tried to build my token calls to replicate that of the ledger canister, but I don’t think it works well enough as a universal standard.
The two options I am considering are 1) a wrapped ICP, like wETH, or 2) An interface that has the same public calls as ICIP-1 to make it interoperable with other tokens, but behind the scenes it interacts directly with the ledger canister. Currently approach #2 seems too complex to have it working exactly the same, so a wrapped approach may be better…
It does seem like we’ll need both wrapped ICP and cycles - the first approach seems more flexible, and likely costs less cycles.
Wrapping and unwrapping ICP will need to call the ledger, which uses AccountIdentifiers (seems like we need a SHA224 in motoko).
The wrapped cycle container also comes with some complexity as a canister is limited to 2^64-1 cycles, so it will need to support dynamic canister creation.
I’ve done some work on a wrapped ICP contract, which I will post and share soon. I think we can get around working with AccountIdentifiers - I originally thought this was just Principal.toText (as the AccountIdentifier type is just Text) but I will look into this.