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

Best practices and recommendations from audit #92

Merged
merged 6 commits into from
Jul 17, 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
29 changes: 16 additions & 13 deletions FungibleToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,14 @@ describe("token integration", async () => {
adminPublicKey: tokenAdmin,
})
await tokenAContract.deploy({
admin: tokenAdmin,
symbol: "tokA",
src: "",
decimals: UInt8.from(9),
src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
})
await tokenAContract.initialize()
await tokenAContract.initialize(
tokenAdmin,
UInt8.from(9),
Bool(true),
)
})

tx.sign([
Expand All @@ -97,13 +99,14 @@ describe("token integration", async () => {
adminPublicKey: tokenBAdmin,
})
await tokenBContract.deploy({
admin: tokenBAdmin,
symbol: "tokB",
src: "",
decimals: UInt8.from(9),
startUnpaused: true,
src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
})
await tokenBContract.initialize()
await tokenBContract.initialize(
tokenBAdmin,
UInt8.from(9),
Bool(false),
)
})

tx.sign([deployer.key, tokenB.key, tokenBAdmin.key])
Expand Down Expand Up @@ -708,7 +711,7 @@ describe("token integration", async () => {
const sendAmount = UInt64.from(100)

it("should mint with a custom admin contract", async () => {
FungibleToken.adminContract = CustomTokenAdmin
FungibleToken.AdminContract = CustomTokenAdmin
const initialBalance = (await tokenBContract.getBalanceOf(sender))
.toBigInt()
const initialCirculating = (await tokenBContract.getCirculating()).toBigInt()
Expand All @@ -733,7 +736,7 @@ describe("token integration", async () => {
(await tokenBContract.getCirculating()).toBigInt(),
initialCirculating + mintAmount.toBigInt(),
)
FungibleToken.adminContract = FungibleTokenAdmin
FungibleToken.AdminContract = FungibleTokenAdmin
})

it("should send tokens without having the custom admin contract", async () => {
Expand Down Expand Up @@ -774,7 +777,7 @@ describe("token integration", async () => {
})

it("should not mint too many B tokens", async () => {
FungibleToken.adminContract = CustomTokenAdmin
FungibleToken.AdminContract = CustomTokenAdmin
await rejects(async () =>
await Mina.transaction({
sender: sender,
Expand All @@ -783,7 +786,7 @@ describe("token integration", async () => {
await tokenBContract.mint(sender, illegalMintAmount)
})
)
FungibleToken.adminContract = FungibleTokenAdmin
FungibleToken.AdminContract = FungibleTokenAdmin
})
it("should not mint too many B tokens using the vanilla admin contract", {
skip: !proofsEnabled,
Expand Down
56 changes: 28 additions & 28 deletions FungibleToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,28 @@ import {
Field,
Int64,
method,
Option,
Permissions,
Provable,
PublicKey,
State,
state,
Struct,
TokenContract,
TokenContractV2,
Types,
UInt64,
UInt8,
} from "o1js"
import { FungibleTokenAdmin, FungibleTokenAdminBase } from "./FungibleTokenAdmin.js"

interface FungibleTokenDeployProps extends Exclude<DeployArgs, undefined> {
/** 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.
* Typically a link to a file on github. */
src: string
/** Number of decimals in a unit */
decimals: UInt8
/** Unless this is set to `true`, the tokens will start in paused mode,
* and will need to be explicitly resumed by calling the `resume()` method.
* You should only set this to `true` in atomic deploys. */
startUnpaused?: boolean
}

export class FungibleToken extends TokenContract {
export class FungibleToken extends TokenContractV2 {
@state(UInt8)
decimals = State<UInt8>()
@state(PublicKey)
Expand All @@ -46,9 +37,9 @@ export class FungibleToken extends TokenContract {
paused = State<Bool>()

// This defines the type of the contract that is used to control access to administrative actions.
// If you want to have a custom contract, overwrite this by setting FungibleToken.adminContract to
// If you want to have a custom contract, overwrite this by setting FungibleToken.AdminContract to
// your own implementation of FungibleTokenAdminBase.
static adminContract: new(...args: any) => FungibleTokenAdminBase = FungibleTokenAdmin
static AdminContract: new(...args: any) => FungibleTokenAdminBase = FungibleTokenAdmin

readonly events = {
SetAdmin: SetAdminEvent,
Expand All @@ -60,24 +51,29 @@ export class FungibleToken extends TokenContract {

async deploy(props: FungibleTokenDeployProps) {
await super.deploy(props)

this.admin.set(props.admin)
this.decimals.set(props.decimals)
this.paused.set(Bool(false))

this.account.tokenSymbol.set(props.symbol)
this.paused.set(Bool(true))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just curious. Do you set the paused flag in the initialized method as well as here in deploy?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably superfluous. We urge people to use an atomic deploy, calling the deploy() functions of the admin and token contract, as well as initialize() all in one transaction. But if they don't, we at least want no transactions to happen before initialise() is called.

this.account.zkappUri.set(props.src)

if (props.startUnpaused) {
this.paused.set(Bool(false))
} else {
this.paused.set(Bool(true))
}
}

// ** Initializes the account for tracking total circulation. */
/** Initializes the account for tracking total circulation.
* @argument {PublicKey} admin - public key where the admin contract is deployed
* @argument {UInt8} decimals - number of decimals for the token
* @argument {Bool} startPaused - if set to `Bool(true), the contract will start in a mode where token minting and transfers are paused. This should be used for non-atomic deployments
*/
@method
async initialize() {
async initialize(
admin: PublicKey,
decimals: UInt8,
startPaused: Bool,
) {
this.account.provedState.requireEquals(Bool(false))
super.init()
this.admin.set(admin)
this.decimals.set(decimals)
this.paused.set(Bool(false))

this.paused.set(startPaused)

const accountUpdate = AccountUpdate.createSigned(this.address, this.deriveTokenId())
let permissions = Permissions.default()
// This is necessary in order to allow token holders to burn.
Expand All @@ -92,7 +88,7 @@ export class FungibleToken extends TokenContract {
return pk
})
this.admin.requireEquals(admin)
return (new FungibleToken.adminContract(admin))
return (new FungibleToken.AdminContract(admin))
}

@method
Expand Down Expand Up @@ -163,6 +159,10 @@ export class FungibleToken extends TokenContract {
assert(updateAllowed.or(permissions.isSome.not()))
}

/** Approve `AccountUpdate`s that have been created outside of the token contract.
*
* @argument {AccountUpdateForest} updates - The `AccountUpdate`s to approve. Note that the forest size is limited by the base token contract, @see TokenContractV2.MAX_ACCOUNT_UPDATES The current limit is 9.
*/
@method
async approveBase(updates: AccountUpdateForest): Promise<void> {
this.paused.getAndRequireEquals().assertFalse()
Expand Down
29 changes: 19 additions & 10 deletions documentation/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,26 @@ The on-chain state is defined as follows:
@state(Bool) paused = State<Bool>()
```

The `deploy` function takes as arguments
The `deploy()` function takes as arguments

- The public key of the account that the admin contract has been deployed to
- A symbol to use as the token symbol
- A string 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

Immediately after deploying the contract -- ideally, in the same transaction -- the contract needs
to be initialized via the `initialize()` method. Its arguments are

- The public key of the account that the admin contract has been deployed to
- A `UInt8` for the number of decimals
- An optional `boolean` to signify whether token transfers should be enabled immediately. Unless
this is supplied and set to `true`, the token contract will be in a paused state initially, and
the `resume()` will need to be called before tokens can be minted or transferred. This is safer if
you have a non-atomic deploy (i.e., if you do not have the admin contract deployed in the same
transaction as the token contract itself).
- A `Bool` to determine whether the token contract should start in paused mode. whether token
transfers should be enabled immediately. If set to `Bool(true)`, the token contract will be in a
paused state initially, and the `resume()` method will need to be called before tokens can be
minted or transferred. This is safer if you have a non-atomic deploy (i.e., if you do not have the
admin contract deployed in the same transaction as the token contract is itself is deployed and
initialized).

and initializes the state of the contract. Initially, the circulating supply is set to zero, as no
tokens have been created yet.
This method initializes the state of the contract. Initially, the circulating supply is set to zero,
as no tokens have been created yet.

## Methods

Expand Down Expand Up @@ -128,3 +133,7 @@ Note that `MintEvent`, `BurnEvent`, and `BalanceChangeEvent` each signal that th
account changes. The difference is that `MintEvent` and `BurnEvent` are emitted when tokens are
minted/burned, and `BalanceChangeEvent` is emitted when a transaction takes tokens from some
addresses, and sends them to others.

[!NOTE] Note that `MintEvent`, `BurnEvent`, and `BalanceChangeEvent` events can be emitted with
`amount = 0`. If you want to track "true" mints/burns/transfers (for example, to maintain a list of
depositors), you will need to filter for non-zero values of `amount`.
36 changes: 22 additions & 14 deletions documentation/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ token contract itself, and initializing the contract

## Deploying an admin contract

The first step is deploying the admin contract via its `deploy()` function.

The admin contract handles permissions for privileged actions, such as minting. It is called by the
token contract whenever a user tries to do a privileged action.

Expand All @@ -21,27 +23,33 @@ If you want to change your admin contract, you can write a contract that `extend
from scratch. Inheriting from `FungibleTokenAdmin` and overwriting specific methods might not work.
You can find an example of a custom admin contract in `FungibleToken.test.ts`.

## Deploying the token contract
The `initialize()` method of `FungibleToken` takes as one argument the address of the admin
contract. If you have written your own admin contract, you will also need to set
`FungibleToken.AdminContract` to that class.

The `deploy` function of `FungibleToken` takes as one argument the address of the admin contract. If
you have written your own admin contract, you will also need to set `FungibleToken.adminContract` to
that class.
[!NOTE] If you do not use the `FungibleToken` class as is, third parties that want to integrate your
token will need to use your custom contract as well.

[!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.
## Initializing and deploying the token contract

## Initializing the token contract
Next, the token contract needs to be deployed, via its `deploy()` function.

After being deployed, the token contract needs to be initialized, by calling the `initialize()`
method. That method creates an account on the chain that will be used to track the current
circulation of the token.
method. That method initializes the contract state, and creates an account on the chain that will be
used to track the current circulation of the token.

[!NOTE] All three steps above can be carried out in a single transaction, or in separate
transactions. [!NOTE] Each of the three steps requires funding a new account on the chain via
`AccountUpdate.fundNewAccount`. [!NOTE] Per default, the token contract will start in paused mode.
If you perform all the steps in one single transaction, you can instead opt for starting it in
non-paused mode. Otherwise, you will need to call `resume()` before any tokens can be minted or
transferred.
transactions. It is highly recommended to have a single transaction with all three steps.

[!NOTE] Unless you have a very good reason, please use one transaction that deploys the admin
contract, deploys the token contract, and calls `initialize()` on the token contract.

[!NOTE] Each of the three steps requires funding a new account on the chain via
`AccountUpdate.fundNewAccount`.

[!NOTE] If you use separate transactions for deploying the admin contract and deploying and
initializing the token contract, you should start the token contract in paused mode, and only call
`resume()` after you have verified that the admin contract has been successfully deployed.

Refer to
[examples/e2e.eg.ts](https://github.com/MinaFoundation/mina-fungible-token/blob/main/examples/e2e.eg.ts)
Expand Down
11 changes: 8 additions & 3 deletions documentation/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ const deployTx = await Mina.transaction({
AccountUpdate.fundNewAccount(deployer, 3)
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 tokenBContract.initialize()
await tokenBContract.initialize(
admin.publicKey,
UInt8.from(9),
Bool(false),
)
})
await deployTx.prove()
deployTx.sign([deployer.key, contract.privateKey, admin.privateKey])
Expand Down Expand Up @@ -93,3 +95,6 @@ the `approveBase()` method of the custom token standard reference implementation
> [!IMPORTANT] When manually constructing `AccountUpdate`s, make sure to order then appropriately in
> the call to `approveBase()`. The contract will not allow flash minting, i.e., tokens cannot be
> received by an account before they have been sent from an account.

[!NOTE] The number of `AccountUpdate`s that you can pass to `approveBase()` is limited by the base
token contract. The current limit is 9.
18 changes: 10 additions & 8 deletions examples/concurrent-transfer.eg.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AccountUpdate, Mina, PrivateKey, PublicKey, UInt64, UInt8 } from "o1js"
import { AccountUpdate, Bool, Mina, PrivateKey, PublicKey, UInt64, UInt8 } from "o1js"
import { setLightnetAccountManagerEndpoint } from "o1js/dist/node/lib/mina/fetch.js"
import { FungibleToken, FungibleTokenAdmin } from "../index.js"

const url = "https://proxy.devnet.minaexplorer.com/graphql"
Expand Down Expand Up @@ -88,17 +89,18 @@ const deployTx = await Mina.transaction({
AccountUpdate.fundNewAccount(feepayer.publicKey, 3)
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),
// We can set `startUnpaused` to true here, because we are doing an atomic deployment
src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
})
await token.initialize(
admin.publicKey,
UInt8.from(9),
// We can set `startPaused` to `Bool(false)` here, because we are doing an atomic deployment
// If you are not deploying the admin and token contracts in the same transaction,
// it is safer to start the tokens paused, and resume them only after verifying that
// the admin contract has been deployed
startUnpaused: true,
})
await token.initialize()
Bool(false),
)
})
await deployTx.prove()
deployTx.sign([feepayer.privateKey, contract.privateKey, admin.privateKey])
Expand Down
17 changes: 9 additions & 8 deletions examples/e2e.eg.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { equal } from "node:assert"
import { AccountUpdate, Mina, PrivateKey, UInt64, UInt8 } from "o1js"
import { AccountUpdate, Bool, Mina, PrivateKey, UInt64, UInt8 } from "o1js"
import { FungibleToken, FungibleTokenAdmin } from "../index.js"

const localChain = await Mina.LocalBlockchain({
Expand All @@ -25,17 +25,18 @@ const deployTx = await Mina.transaction({
AccountUpdate.fundNewAccount(deployer, 3)
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),
// We can set `startUnpaused` to true here, because we are doing an atomic deployment
src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
})
await token.initialize(
admin.publicKey,
UInt8.from(9),
// We can set `startPaused` to `Bool(false)` here, because we are doing an atomic deployment
// If you are not deploying the admin and token contracts in the same transaction,
// it is safer to start the tokens paused, and resume them only after verifying that
// the admin contract has been deployed
startUnpaused: true,
})
await token.initialize()
Bool(false),
)
})
await deployTx.prove()
deployTx.sign([deployer.key, contract.privateKey, admin.privateKey])
Expand Down
Loading