-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
speccing discussion #46
base: main
Are you sure you want to change the base?
Changes from all commits
f71f824
1f6e2ce
f50656b
a3aec4c
7abb7b2
3737116
ffe0a67
da26165
0c14545
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,353 @@ | ||
# Mina Fungible _Tokenbase_[^tokenbase] Discussion | ||
|
||
This document is part specification and part meta-discussion. The specification portion touches on a | ||
design for a "Fungible Tokenbase", which outlines an approach to modeling fungible tokens on Mina. | ||
This includes interfaces for common actions, descriptions of how actions should be reduced into | ||
subsequent states, and the fungible token lifecycle. It describes the minimum functionality with | ||
which admin contracts can compose use-case-specific fungible tokens while still providing a common | ||
interface against which the community (namely wallets) can develop. The process of speccing this out | ||
led to critical questions about not only the possibility of implementation, but also the ultimate | ||
purpose of the specification itself. | ||
|
||
<!-- START doctoc generated TOC please keep comment here to allow auto update --> | ||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> | ||
|
||
- [Mina Fungible _Tokenbase_\[^tokenbase\] Discussion](#mina-fungible-tokenbasetokenbase-discussion) | ||
- [Purpose of Specification](#purpose-of-specification) | ||
- [Long-term Consideration](#long-term-consideration) | ||
- [Near-term Recommendation](#near-term-recommendation) | ||
- [Design](#design) | ||
- [Goals](#goals) | ||
- [Non-goals](#non-goals) | ||
- [Actions](#actions) | ||
- [`Create`](#create) | ||
- [`Destroy`](#destroy) | ||
- [`SetAdmin`](#setadmin) | ||
- [`Mint`](#mint) | ||
- [`Burn`](#burn) | ||
- [`Transfer`](#transfer) | ||
- [`Allocate`](#allocate) | ||
- [`Deallocate`](#deallocate) | ||
- [`Freeze`](#freeze) | ||
- [`Thaw`](#thaw) | ||
- [`SetMetadata`](#setmetadata) | ||
- [Architecture](#architecture) | ||
- [Actions and Reducers Drawbacks](#actions-and-reducers-drawbacks) | ||
- [Closing Note](#closing-note) | ||
|
||
<!-- END doctoc generated TOC please keep comment here to allow auto update --> | ||
|
||
## Purpose of Specification | ||
|
||
Typically a specification would describe all the constraints that affect how one integrates with the | ||
specified software. In the context of Ethereum, [EIPs](https://eips.ethereum.org/) have been | ||
essential to the creation of a wide range of tools and a web of interconnected contracts. This has | ||
been possible because Ethereum contract execution happens on-chain. The creators of tools, dependent | ||
contracts, wallets and other such programs need only be aware of the types and capabilities of their | ||
target contract. **This is not true of Mina**. To interact with a Mina contract, one needs to run | ||
the contract code themselves. Each instruction affects how contract results are ultimately proven. | ||
Interacting with a contract whose method signatures abide by a given spec is not necessarily aligned | ||
with the desired behavior; in the world of Mina, the implementation is––more or less––the spec. This | ||
complicates the creation of tools that need to interact with Mina contracts, as these tools need the | ||
actual contract. | ||
|
||
### Long-term Consideration | ||
|
||
Some missing pieces may need to come into place before we can design contract specs which solve the | ||
problems that specs are meant to solve. Some ideas regarding what might be helpful: | ||
|
||
- A system for extracting and sharing metadata about the provable properties of a contract. | ||
- A tool which asserts that a given unknown contract has the specified properties. This would enable | ||
the creation and confirmation of "spec"-compliant contracts in the Mina sense of the word "spec." | ||
|
||
## Near-term Recommendation | ||
|
||
Speccing via TypeScript interfaces alone won't necessarily benefit the community beyond serving as | ||
inspiration for their own implementations. That being said, there is still an unmet need to provide | ||
developers a happy path for managing fungible tokens on Mina. To deliver a worthwhile fungible-token | ||
solution, we could create and deploy a contract and corresponding service that satisfy common use | ||
cases. Although not a "spec", this contract and service could provide common APIs with which | ||
developers could manage fungible tokens and administrate those tokens from their own, | ||
use-case-specific contracts. This solution could serve as a stopgap while we flesh out how the | ||
protocol itself can eventually satisfy all implementation requirements (the current shortcomings of | ||
which we touch on below). This could-be stopgap service will be the focus of the remainder of this | ||
document. | ||
|
||
## Design | ||
|
||
### Goals | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should there be an "Upgradable" goal? I suspect it's important since a proof is totally dependent on specific code versions. Is a consumer doomed to be pinned to a specific version indefinitely? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a great question. Upgradability in Mina seems challenging. I believe one could use the approach of setting a target contract address in the state of a proxy contract. We'd want to scope down the proxy contract's permissions. The confusing part comes from whether the data pre-upgrade is still provable after the given upgrade. Definitely worth testing (#48). |
||
|
||
- **Wallet-friendly**: wallets should be able to display and prove data of `FungibleTokenbase`s | ||
without needing to dynamically import 3rd party contract code. | ||
Comment on lines
+80
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Mina seems geared towards a micro-service architecture. I suspect it is necessary to encapsulate the custom code for tokens behind a standardized REST or RPC API that the generic wallet can invoke without needing to know the implementation details. Otherwise, every token will have to be compiled into the code of the wallet which won't scale. Only other alternative I can think of is to have a registry of addressable contract code that is fetched on the fly and evaluated by the wallet. That is a big security risk though, would need to have a solid sandbox for running untrusted code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I 100% agree. In the case of this proposal, the Fungible Tokenbase contract code would be depended upon by wallets, which would use it to prove balances. However, any admin contracts would not be represented within the wallet. Related work to be taken on: https://github.com/MinaFoundation/Core-Grants/pull/12/files |
||
- **Forgo Deployment**: it should be possible to create custom token types without the deployment of | ||
a custom fungible token contract. Token types are just data after all; token-type-specific | ||
contracts––at least for would-be "standard" functionality––are unnecessary. By cutting out the | ||
unnecessary contract deployment step, we spare the developer of needing to generate and fund a new | ||
account for the contract. | ||
- **Extensible**: the `FungibleTokenbase` contract should offer all functionality necessary for a | ||
3rd-party contract to become the admin and craft use-case-specific functionality. | ||
- **Scalability**: the contract should support concurrent transactions at scale. | ||
- **Off-chain State Management API**: the contract-accompanying service should facilitate | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you have a specific goal around whether the account balance will be public or private? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great question. Imo it should be private by default. Which do you think is preferable? |
||
interaction with persisted off-chain state (computing new commitments, paginating lists of tokens | ||
and token accounts, reading allocation lists by various keys (allocation ID, allocator ID, | ||
allocated ID, token ID), misc. | ||
|
||
### Non-goals | ||
|
||
- **Fine-grained Authorization**: there is no separation of administrative role by action. For | ||
instance, the token admin has permission to freeze and account as well as burn tokens from an | ||
account. Other systems may separate such roles for added security. Fungible Tokenbase token admins | ||
can dispatch all actions. Meanwhile non-admin accounts can manipulate their token-specific | ||
accounts (unless frozen by the token admin). To implement more advanced authorization, developers | ||
can create and set as admin a contract that delegates back to the token base. | ||
- **Account Purging or "Sufficiency"**: the fungible token base does not represent existential | ||
deposits nor weight custom tokens against native tokens such as Mina. There is no minimum balance. | ||
When a token account has no funds, it is no longer represented in state. If it "receives" funds | ||
(either from a transfer or from an allocation) it exists. Alternative assets can be utilized in | ||
admin contracts if need be. However, the Fungible Tokenbase does not assert a relationship between | ||
the value of taking various actions and Mina nor native custom tokens. | ||
|
||
### Actions | ||
|
||
It seems that actions and reducers are the recommended path for new o1js contracts, as this enables | ||
multiple actions to be queued in a single block, and therefore concurrent transactions. Actions are | ||
used to reduce a new state whenever a `Commit` action is dispatched. This means that users can run | ||
many commands in rapid succession while still ensuring inclusion later. This lazy state model is | ||
seemingly key to contract scalability. Therefore, the `FungibleTokenbase` contract API is described | ||
via action interfaces. | ||
|
||
> Note: the resulting state change descriptions are imprecise, as it is still unclear what | ||
> representations should live off-chain. For instance, the contract depends on representations of | ||
> potentially large mappings. Therefore, many of the actions will be accompanied by merkle witnesses | ||
> and proofs. | ||
|
||
> Note: errors are not yet represented. | ||
|
||
> Note: the actions + reducers approach has [several drawbacks](#actions-and-reducers-drawbacks) | ||
> which may impact the feasibility of a scalable `FungibleTokenbase` implementation. | ||
|
||
#### `Create` | ||
|
||
Our first action is to create a new token type. Everyone is authorized to dispatch this action. | ||
|
||
```ts | ||
interface Create { | ||
/** A unique ID of TBD type––perhaps a `UInt64`. */ | ||
id: TokenId | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest to autogenerate it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a way to do so without breaking commutativity? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. idk how it breaks commutativity. tokenID can be just a hash of some unique struct There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How would this ID be generated within the contract? I'm not opposed to it, I just don't know how it could be implemented. I've contemplated tracking a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hash of smth There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hash of previously created token id!)))) |
||
/** The initial amount of the new token. */ | ||
supply: UInt64 | ||
/** The account that administrates this token (can ultimately be contract). */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure if a contract can be an owner, because in mina you can't check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oof that's rough. Good to know though. |
||
admin: PublicKey | ||
/** The merkle root of some off-chain metadata (for instance, the token symbol). */ | ||
metadata: Field | ||
/** The number of decimal places for which to account. */ | ||
decimals: UInt8 | ||
} | ||
``` | ||
|
||
This action should result in the addition of a new entry to a mapping from token ID to `TokenInfo`. | ||
|
||
```ts | ||
interface TokenInfo { | ||
/** The current admin of the token. */ | ||
admin: AccountId | ||
/** The current supply of the token */ | ||
supply: UInt64 | ||
/** The merkle root representing the accounts holding the current token. */ | ||
accounts: Field | ||
/** The merkle root representing the metadata. */ | ||
metadata: Field | ||
} | ||
``` | ||
|
||
This action should also initialize the admin account within the token info. Aka. | ||
`TokenInfo["accounts"]` must also correspond to an off-chain mapping (in this case from `PublicKey` | ||
to `AccountInfo`). | ||
|
||
```ts | ||
export interface AccountInfo { | ||
/** The balance of the current user in the current token. */ | ||
balance: UInt64 | ||
/** Whether the current account is frozen for the current token. */ | ||
frozen: Bool | ||
} | ||
``` | ||
|
||
#### `Destroy` | ||
|
||
The inverse of the `Create` action is `Destroy`, which accepts a single prop `token`, the ID of the | ||
token to destroy. | ||
|
||
```ts | ||
interface Destroy { | ||
/** The ID of the token to destroy */ | ||
token: TokenId | ||
} | ||
``` | ||
|
||
#### `SetAdmin` | ||
|
||
The admin of a token has the ability to... | ||
|
||
- Set a new admin. | ||
- Mint and burn tokens to and from any account. | ||
- Transfer between any two accounts. | ||
- Freeze and thaw any accounts. | ||
- Destroy the token of which they are admin. | ||
- Allocate and deallocate funds between any two accounts. | ||
|
||
Contracts can administrate a token type / implement their own behavior around authorization and the | ||
dispatching of actions. | ||
|
||
```ts | ||
interface SetAdmin { | ||
/** The ID of the token on which to set the admin. */ | ||
token: TokenId | ||
/** The new token admin. */ | ||
admin: PublicKey | ||
} | ||
``` | ||
|
||
#### `Mint` | ||
|
||
An admin can dispatch the `Mint` action, which adds the specified amount to the admin's account for | ||
the given token. This also adds to the token supply in the token info. | ||
|
||
```ts | ||
interface Mint { | ||
/** The ID of the token we want to mint. */ | ||
token: TokenId | ||
/** The beneficiary of the new tokens. */ | ||
to: PublicKey | ||
/** The amount of the new token we want to mint. */ | ||
amount: UInt64 | ||
} | ||
``` | ||
|
||
#### `Burn` | ||
|
||
```ts | ||
interface Burn { | ||
/** The ID of the token to burn. */ | ||
token: TokenId | ||
/** The account from which to burn. */ | ||
from: PublicKey | ||
/** The amount to burn. */ | ||
amount: UInt64 | ||
} | ||
``` | ||
|
||
#### `Transfer` | ||
|
||
```ts | ||
export interface Transfer { | ||
/** The ID of the token to transfer. */ | ||
token: TokenId | ||
/** The account from which to transfer the tokens. */ | ||
from: PublicKey | ||
/** The account which should receive the tokens. */ | ||
to: PublicKey | ||
/** The amount of tokens to transfer. */ | ||
amount: UInt64 | ||
} | ||
``` | ||
|
||
#### `Allocate` | ||
|
||
Allocating is similar to transferring, except that the funds remain under the control of the `from` | ||
account until the `to` account explicitly transfers out those funds. This also ensures that the | ||
funds remain untouched should the `from` account balance dip beneath the amount of the allocation. | ||
In a sense, it creates a pot and sets it to the side for a particular purpose / account. | ||
|
||
```ts | ||
interface Allocate { | ||
/** The unique ID of the allocation. */ | ||
id: AllocationId | ||
/** The ID of the token. */ | ||
token: TokenId | ||
/** The account from which the allocation is made. */ | ||
from: PublicKey | ||
/** The account which has the ability to transfer out the allocation. */ | ||
to: PublicKey | ||
/** The amount to allocate. */ | ||
amount: UInt64 | ||
} | ||
``` | ||
|
||
#### `Deallocate` | ||
|
||
The `from` account can reclaim the funds by dispatching a deallocation. | ||
|
||
```ts | ||
interface Deallocate { | ||
/** The ID of the allocation to deallocate. */ | ||
allocation: AllocationId | ||
} | ||
``` | ||
|
||
#### `Freeze` | ||
|
||
In some cases, the token administrator may want to freeze an account, thereby disabling transfers | ||
and allocations. | ||
|
||
```ts | ||
interface Freeze { | ||
/** The ID of the token of the target account. */ | ||
token: TokenId | ||
/** The account to be frozen. */ | ||
who: PublicKey | ||
} | ||
``` | ||
|
||
#### `Thaw` | ||
|
||
To thaw an account means to reenable transfers and allocations after freezing has occurred. The | ||
action payload is identical to its counterpart `Freeze`. | ||
|
||
```ts | ||
interface Freeze { | ||
/** The ID of the token of the target account. */ | ||
token: TokenId | ||
/** The account to be thawed. */ | ||
who: PublicKey | ||
} | ||
``` | ||
|
||
#### `SetMetadata` | ||
|
||
This action lets you associate the token with some arbitrary `Field`, which could be a merkle root. | ||
|
||
```ts | ||
interface SetMetadata { | ||
/** The token, the metadata of which is to be set. */ | ||
token: TokenId | ||
/** The metadata. */ | ||
metadata: Field | ||
} | ||
``` | ||
|
||
## Architecture | ||
|
||
### Actions and Reducers Drawbacks | ||
|
||
There are several drawbacks to the actions/reducers approach. | ||
|
||
- There is a need for a commit method to reduce the actions and previous state into the next state. | ||
This needs to be explicitly called. This cannot be committed in the same block as other actions of | ||
the same `FungibleTokenbase`. | ||
- The action sequence must be processed in such a way as to preserve commutativity, which is | ||
difficult for the use cases of a fungible token account manager. Users may submit actions which | ||
touch on the same state. | ||
- Actions which alter the merkle root (in order to represent mappings) must be atomic. There is no | ||
way to commutatively sequence their application to state within the reducer. | ||
|
||
## Closing Note | ||
|
||
The majority of the `FungibleTokenbase` experience would likely take shape off-chain. The state | ||
could be lazily pushed to a contract on Mina, but much of the core behavior would need to be | ||
off-chain; it is uncertain whether that behavior could be modeled such that it is fully provable. To | ||
reiterate: **this API could serve as a north star for what we'd like to be able to express in Mina | ||
contracts** as well as providing a smooth DX to satisfy the ecosystem's Fungible Token use cases. | ||
|
||
[^tokenbase]: _Tokenbase_ is not a standard term, yet it seemed fitting for this system. That being | ||
said, alternative terminology might be better. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like an important point worth focusing on. It effectively means that a contract's code must either:
It hints that the version of a function is the most important part of the contract. It might be worth exploring how semantic versions,
patch
,minor
andmajor
, apply to changes in contract code. Is every change a "breaking" change because the proof will be different? What do consumers do when encountering a breaking change?This looks like a dependency hell situation. Any tiny change in a function or change in its upstream dependencies could break the contract. It reminds me of Unison Lang's content-addressable functions where references are transformed into globally unique addresses to support branched versioning of programs. Two versions of the same function can safely run in the same unison app because they would have different addresses.
I think it ultimately means that having an easy way to upgrade a package is top priority or otherwise risk a situation where code can't be improved.
My question is: should this contract include an upgrade mechanism first-class?
Or is this out of scope?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spot-on. I agree with the stopgap approach you suggested of placing the contract behind a service. Otherwise, seemingly too much difficulty crops up for anyone who wants to interact with the contract. This difficulty is compounded as we get into large amounts of off-chain storage and upgradability.