Skip to content
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

Update documentation, and minor changes from rfc review #64

Merged
merged 4 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions FungibleToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import {
FungibleTokenAdminDeployProps,
} from "./index.js"

const proofsEnabled = process.argv.indexOf("--proofs-disabled") === -1
const proofsEnabled = process.env.SKIP_PROOFS !== "true"
if (!proofsEnabled) console.log("Skipping proof generation in tests.")

const localChain = await Mina.LocalBlockchain({
proofsEnabled,
Expand All @@ -32,7 +33,7 @@ const localChain = await Mina.LocalBlockchain({
Mina.setActiveInstance(localChain)

describe("token integration", async () => {
if (proofsEnabled) {
{
await FungibleToken.compile()
await ThirdParty.compile()
await FungibleTokenAdmin.compile()
Expand Down
13 changes: 7 additions & 6 deletions FungibleToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,21 @@ import {
} from "o1js"
import { FungibleTokenAdmin, FungibleTokenAdminBase } from "./FungibleTokenAdmin.js"

export interface FungibleTokenDeployProps extends Exclude<DeployArgs, undefined> {
interface FungibleTokenDeployProps extends Exclude<DeployArgs, undefined> {
kantp marked this conversation as resolved.
Show resolved Hide resolved
/** Address of the contract controlling permissions for administrative actions */
admin: PublicKey
/** The token symbol. */
symbol: string
/** A source code reference, which is placed within the `zkappUri` of the contract account. */
/** A source code reference, which is placed within the `zkappUri` of the contract account.
* Typically a link to a file on github. */
src: string
/** Number of decimals in a unit */
decimals: UInt8
}

export class FungibleToken extends TokenContract {
@state(UInt8)
decimals = State<UInt8>() // UInt64.from(9)
decimals = State<UInt8>()
@state(PublicKey)
admin = State<PublicKey>()
@state(UInt64)
Expand Down Expand Up @@ -95,7 +96,7 @@ export class FungibleToken extends TokenContract {
}

@method.returns(AccountUpdate)
async mint(recipient: PublicKey, amount: UInt64) {
async mint(recipient: PublicKey, amount: UInt64): Promise<AccountUpdate> {
this.paused.getAndRequireEquals().assertFalse()
const accountUpdate = this.internal.mint({ address: recipient, amount })
const adminContract = await this.getAdminContract()
Expand All @@ -108,7 +109,7 @@ export class FungibleToken extends TokenContract {
}

@method.returns(AccountUpdate)
async burn(from: PublicKey, amount: UInt64) {
async burn(from: PublicKey, amount: UInt64): Promise<AccountUpdate> {
this.paused.getAndRequireEquals().assertFalse()
const accountUpdate = this.internal.burn({ address: from, amount })
this.emitEvent("Burn", new BurnEvent({ from, amount }))
Expand Down Expand Up @@ -221,7 +222,7 @@ export class FungibleToken extends TokenContract {
}

@method.returns(UInt8)
async getDecimals() {
async getDecimals(): Promise<UInt8> {
return this.decimals.getAndRequireEquals()
}
}
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ awaiting an audit of the code before removing the beta status.
npm run test
```

If you want disable proof generation during testing, you can do so via

```sh
SKIP_PROOFS=true npm run test
```

The tests will run much faster that way, which is nice when you're testing locally while developing.
Note that this will skip one test does

## Running [Examples](./examples)

```sh
Expand Down
104 changes: 59 additions & 45 deletions documentation/api.md
Original file line number Diff line number Diff line change
@@ -1,80 +1,92 @@
# API overview

The token standard implementation is a Token Manager zkApp that is split in 2 parts: low-level and
high-level one.
The token standard implementation provides a smart contract `FungibleToken` that can be deployed as
the token owner for a new token. It provides all the user facing functionality that is expected of a
fungible token: creating, transferring, and destroying tokens, as well as querying balances and the
overall amount of tokens.

The low-level implementation is included in `o1js` library `TokenContract` abstract class. See the
overview in the o1js
[Custom Tokens tutorial](https://docs.minaprotocol.com/zkapps/o1js/custom-tokens)
Using the standard means using this particular, unmodified, contract. The reason that altering the
contract is considered deviating from the standard is the off-chain execution model of MINA: a third
party (wallet, exchange, etc.) that wants to integrate a token needs to have access to and execute
the code of the token owner contract in order to interact with the token. Agreeing on one particular
implementation reduces the burden of integration significantly.

> [!WARNING] Please note that this is a beta release. The implementation will change soon. The API
> may also change in future.
In order to allow for some customization without changing the token owner contract, we delegate some
functionality to a secondary admin contract, called `FungibleTokenAdmin`. This contract controls
access to privileged operations such as minting, pausing/resuming transfers, or changing the admin
contract itself. This construction allows you to set the rules for monetary expansion, without
changing the token owner contract itself. Since the admin contract will only be called from methods
of the token contract that are not meant to be called by regular users, the code of the admin
contract does not need to be integrated into wallets or other third party applications.

The high-level part inherits from the `TokenContract` class and has following user-facing features:
is a Token Manager zkApp that is split in 2 parts: low-level and high-level one.

## On-chain State, `decimals` and deploy arguments
## The `FungibleToken` contract

## On-chain State and deploy arguments

The on-chain state is defined as follows:

```ts
@state(PublicKey) public owner = State<PublicKey>();
@state(UInt64) public supply = State<UInt64>();
@state(UInt64) public circulating = State<UInt64>();
@state(UInt8) decimals = State<UInt8>()
@state(PublicKey) admin = State<PublicKey>()
@state(UInt64) private circulating = State<UInt64>()
@state(Field) actionState = State<Field>()
@state(Bool) paused = State<Bool>()
```

- `owner` is set on deployment, and some of token functionality requires an admin signature.

If you want to implement admin-only method, just call `this.ensureOwnerSignature()` helper in the
method you want to protect.

- `supply` defines a maximum amount of tokens to exist. It is set on deployment and can be modified
with `setSupply()` function (can be called by admin only)

- `circulating` tracks the total amount in circulation. When new tokens are minted, the
`circulating` increases by an amount minted.
The `deploy` function takes as arguments

- The `decimals` is a constant, that defines where to place the decimal comma in the token amounts.
- The public key of the account that the admin contract has been deployed to
- A symbol to use as the token symbol
- A string pointing to the source code of the contract -- when following the standard, this should
point to the source of the standard implementation on github
- A `UInt8` for the number of decimals

- The `deploy()` function requires `owner` and `supply` to be passed as parameters.

- Along with state variables initial values, the `deploy()` function also takes `symbol` (to set
`account.tokenSymbol`) and `src` (to set `account.zkappUri`)
and initializes the state of the contract. Initially, the circulating supply is set to zero, as no
tokens have been created yet.

## Methods

Methods that can be called only by admin are:
The user facing methods of `FungibleToken` are

```ts
mint(address: PublicKey, amount: UInt64)
setTotalSupply(amount: UInt64)
setOwner(owner: PublicKey)
@method.returns(AccountUpdate) async burn(from: PublicKey, amount: UInt64): Promise<AccountUpdate>

@method async transfer(from: PublicKey, to: PublicKey, amount: UInt64)
@method async approveBase(updates: AccountUpdateForest): Promise<void>
@method.returns(UInt64) async getBalanceOf(address: PublicKey): Promise<UInt64>
@method.returns(UInt64) async getCirculating(): Promise<UInt64>
@method async updateCirculating()
@method.returns(UInt8) async getDecimals(): Promise<UInt8>
```

Transfer and burn functionality is available by following methods:
The following methods call the admin account for permission, and are not supposed to be called by
regular users

```ts
transfer(from: PublicKey, to: PublicKey, amount: UInt64)
burn(from: PublicKey, amount: UInt64)
@method async setAdmin(admin: PublicKey)
@method.returns(AccountUpdate) async mint(recipient: PublicKey, amount: UInt64): Promise<AccountUpdate>
@method async pause()
@method async resume()
```

Helper methods for reading state variables and account balance
### Minting, burning, and updating the circulating supply

```ts
getBalanceOf(address: PublicKey)
getSupply()
getCirculating()
getDecimals()
```
In order to allow multiple minting/burning transactions in a single block, we use the
actions/reducer model of MINA. The `mint` and `burn` methods will modify the token balance in the
specified account. But instead of directly modifying the value of `circulating` in the contract
state, they will instead dispatches an action that instructs the reducer to modify the state. The
method `calculateCirculating` collects all the actions and updates the state of the contract.

## Events

On each token operation, the event is emitted. The events are declared as follows:
The following events are emitted from `FungibleToken` when appropriate:

```ts
events = {
SetOwner: PublicKey,
SetAdmin: PublicKey,
Mint: MintEvent,
SetSupply: UInt64,
Burn: BurnEvent,
Transfer: TransferEvent,
}
Expand All @@ -96,4 +108,6 @@ class TransferEvent extends Struct({
}) {}
```

That completes a review of a fungible token.
Note that `approveBase` does not emit an event. Thus, transfers where the account updates have been
constructed externally to `FungibleToken` will not have an event emitted by the `FungibleToken`
contract.
56 changes: 9 additions & 47 deletions documentation/deploy.md
Original file line number Diff line number Diff line change
@@ -1,54 +1,16 @@
# Deploy

To create a token manager smart contract, inherit your smart contract from base custom token
implementation, or use the `FungibleToken` directly
To create a new token, deploy the `FungibleToken` contract. You will need an admin account as well,
to control permissions. You can either use the supplied contract `FungibleTokenAdmin`, or write your
own contract that implements `FungibleTokenAdminBase`. An example can be found in
`FungibleToken.test.ts`, where a non-standard admin contract is implemented (`CustomTokenAdmin`,
implementing a minting policy that resembles a faucet).

```ts
import { FungibleToken } from "mina-fungible-token"
[!NOTE] Note that you have to write the admin contract from scratch. Inheriting from
`FungibleTokenAdmin` and overwriting specific methods might not work.

class MyToken extends FungibleToken {}
```

> [!NOTE] If you inherit from `FungibleToken` to override some functionality, you will need to
> compile both parent and child contracts to be able to prove code for both of them

To deploy a token manager contract, create and compile the token contract instance, then create,
prove and sign the deploy transaction:

```ts
await FungibleToken.compile()
await MyToken.compile()

const {
privateKey: tokenKey,
publicKey: tokenAddress,
} = PrivateKey.randomKeypair()
const token = new MyToken(tokenAddress)

// paste the private key of the deployer and admin account here
const deployerKey = PrivateKey.fromBase58("...")
const ownerKey = PrivateKey.fromBase58("...")
const owner = PublicKey.fromPrivateKey(ownerKey)
const deployer = PublicKey.fromPrivateKey(deployerKey)

const supply = UInt64.from(21_000_000)
const symbol = "MYTKN"
const src = "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts"

const fee = 1e8

const tx = await Mina.transaction({ sender: deployer, fee }, () => {
AccountUpdate.fundNewAccount(deployer, 1)
token.deploy(owner, supply, symbol, src)
})

tx.sign([deployerKey, tokenKey])
await tx.prove()
await tx.send()
```

For this and following samples to work, make sure you have enough funds on deployer and admin
accounts.
[!NOTE] If you do not use the `FungibleToken` as is, third parties that want to integrate your token
will need to use your custom contract as well.

Refer to
[examples/e2e.eg.ts](https://github.com/MinaFoundation/mina-fungible-token/blob/main/examples/e2e.eg.ts)
Expand Down
50 changes: 25 additions & 25 deletions documentation/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Mina natively supports custom tokens
([MIP-4](https://github.com/MinaProtocol/MIPs/blob/main/MIPS/mip-zkapps.md#token-mechanics)). Each
account on Mina can correspond to a custom token.

To create a new token, one creates a smart contract, which becomes the manager for the token, and
uses that contract to set the rules around how the token can be minted, burned and transferred. The
To create a new token, one creates a smart contract, which becomes the owner for the token, and uses
that contract to set the rules around how the token can be minted, burned and transferred. The
contract may also set a token symbol. Uniqueness is not enforced for token names. Instead the public
key of the contract is used to derive the token's unique identifier.

Expand All @@ -15,26 +15,28 @@ The
[`mina-fungible-token` repo's e2e example](https://github.com/MinaFoundation/mina-fungible-token/blob/main/examples/e2e.eg.ts)
showcases the entire lifecycle of a token.

After running `npm i mina-fungible-token`, import the `FungibleToken` contract and deploy it like
so.
After running `npm i mina-fungible-token`, import the `FungibleToken` and `FungibleTokenAdmin`
contracts and deploy them:

```ts
const token = new FungibleToken(contract.publicKey)
const adminContract = new FungibleTokenAdmin(admin.publicKey)

const deployTx = await Mina.transaction({
sender: deployer.publicKey,
sender: deployer,
fee,
}, () => {
AccountUpdate.fundNewAccount(deployer.publicKey, 1)
token.deploy({
owner: owner.publicKey,
supply: UInt64.from(10_000_000_000_000),
}, async () => {
AccountUpdate.fundNewAccount(deployer, 2)
await adminContract.deploy({ adminPublicKey: admin.publicKey })
await token.deploy({
admin: admin.publicKey,
symbol: "abc",
src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/examples/e2e.eg.ts",
decimals: UInt8.from(9),
})
})
await deployTx.prove()
deployTx.sign([deployer.privateKey, contract.privateKey])
deployTx.sign([deployer.key, contract.privateKey, admin.privateKey])
await deployTx.send()
```

Expand All @@ -44,25 +46,26 @@ await deployTx.send()

How is this custom token mechanism implemented in Mina?

### Token Manager
### Token Owner Account

The token manager account is a contract with the following capabilities.
The token owner account is a contract with the following capabilities.

- Set a token symbol (also called token name) for its token. Uniqueness is not enforced for token
names because the public key of the manager account is used to derive a unique identifier for each
names because the public key of the owner account is used to derive a unique identifier for each
token.
- Mint new tokens. The zkApp updates an account's balance by adding the newly created tokens to it.
You can send minted tokens to any existing account in the network.
- Burn tokens (the opposite of minting). Burning tokens deducts the balance of a certain address by
the specified amount. A zkApp cannot burn more tokens than the specified account has.
- Send tokens between two accounts. Any account can initiate a transfer, and the transfer must be
approved by a Token Manager zkApp (see [Approval mechanism](#approval-mechanism)).
- Send tokens between two accounts. There are two ways to initiate a transfer: either, the token
owner can create the account updates directly (via the `transfer` method), or the account updates
can be created externally, and then approved by the token owner (see
[Approval mechanism](#approval-mechanism)).

### Token Account

Token accounts are like regular accounts, but they hold a balance of a specific custom token instead
of MINA. A token account is created from an existing account and is specified by a public key _and_
a token id.
of MINA. A token account is specified by a public key _and_ a token id.

Token accounts are specific for each type of custom token, so a single public key can have many
different token accounts.
Expand All @@ -78,13 +81,10 @@ transaction denoted with a custom token.
Token ids are unique identifiers that distinguish between different types of custom tokens. Custom
token identifiers are globally unique across the entire network.

Token ids are derived from a Token Manager zkApp. Use `deriveTokenId()` function to get id of a
token.
Token ids are derived from a Token Owner account. Use the `deriveTokenId()` function to get the id
of a token.

### Approval mechanism

Sending tokens between two accounts must be approved by a Token Manager zkApp. This can be done with
`approveBase()` method of the custom token standard reference implementation.

If you customize the `transfer()` function or constructing `AccountUpdate`s for sending tokens
manually, don't forget to call `approveBase()`.
Sending tokens between two accounts must be approved by a Token Owner zkApp. This can be done with
the `approveBase()` method of the custom token standard reference implementation.
Loading