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

Allow updating the verification key (requiring a signature from the admin) #113

Merged
merged 3 commits into from
Dec 13, 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
73 changes: 73 additions & 0 deletions FungibleToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
state,
UInt64,
UInt8,
VerificationKey,
} from "o1js"
import {
FungibleToken,
Expand All @@ -34,6 +35,10 @@ const localChain = await Mina.LocalBlockchain({
Mina.setActiveInstance(localChain)

describe("token integration", async () => {
const fungibleTokenVerificationKeyData = await FungibleToken.compile()
const fungibleTokenVerificationKey = VerificationKey.fromValue(
fungibleTokenVerificationKeyData.verificationKey,
)
{
await FungibleToken.compile()
await ThirdParty.compile()
Expand Down Expand Up @@ -72,6 +77,7 @@ describe("token integration", async () => {
await tokenAContract.deploy({
symbol: "tokA",
src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
allowUpdates: true,
})
await tokenAContract.initialize(
tokenAdmin,
Expand All @@ -90,6 +96,41 @@ describe("token integration", async () => {
await tx.send()
})

it("should reject a change of the verification key without an admin signature", async () => {
const tx = await Mina.transaction({
sender: deployer,
fee: 1e8,
}, async () => {
await tokenAContract.updateVerificationKey(fungibleTokenVerificationKey)
})

tx.sign([deployer.key])

await tx.prove()
await rejects(
async () => await tx.send(),
(err: Error) => {
if (err.message.includes("the required authorization was not provided or is invalid.")) {
return true
} else return false
},
)
})

it("should allow a change of the verification key", async () => {
const tx = await Mina.transaction({
sender: deployer,
fee: 1e8,
}, async () => {
await tokenAContract.updateVerificationKey(fungibleTokenVerificationKey)
})

tx.sign([deployer.key, tokenAdmin.key])

await tx.prove()
await tx.send()
})

it("should deploy token contract B", async () => {
const tx = await Mina.transaction({
sender: deployer,
Expand All @@ -102,6 +143,7 @@ describe("token integration", async () => {
await tokenBContract.deploy({
symbol: "tokB",
src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
allowUpdates: false,
})
await tokenBContract.initialize(
tokenBAdmin,
Expand All @@ -116,6 +158,31 @@ describe("token integration", async () => {
await tx.send()
})

it("should not allow a change of the verification key if allowUpdates is false", async () => {
const tx = await Mina.transaction({
sender: sender,
fee: 1e8,
}, async () => {
await tokenBContract.updateVerificationKey(await VerificationKey.dummy())
})

tx.sign([sender.key, tokenBAdmin.key])

await tx.prove()
await rejects(
async () => await tx.send(),
(err: Error) => {
if (
err.message.includes(
"Cannot update field 'verificationKey' because permission for this field is 'Impossible'",
)
) {
return true
} else return false
},
)
})

it("should deploy a third party contract", async () => {
const tx = await Mina.transaction({
sender: deployer,
Expand Down Expand Up @@ -936,6 +1003,12 @@ class CustomTokenAdmin extends SmartContract implements FungibleTokenAdminBase {
this.ensureAdminSignature()
return Bool(true)
}

@method.returns(Bool)
public async canChangeVerificationKey(_vk: VerificationKey): Promise<Bool> {
this.ensureAdminSignature()
return Bool(true)
}
}

export default class ThirdParty extends SmartContract {
Expand Down
12 changes: 10 additions & 2 deletions FungibleToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ interface FungibleTokenDeployProps extends Exclude<DeployArgs, undefined> {
/** A source code reference, which is placed within the `zkappUri` of the contract account.
* Typically a link to a file on github. */
src: string
/** Setting this to `true` will allow changing the verification key later with a signature from the deployer. This will allow updating the token contract at a later stage, for instance to react to an update of the o1js library.
* Setting it to `false` will make changes to the contract impossible, unless there is a backward incompatible change to the protocol. (see https://docs.minaprotocol.com/zkapps/writing-a-zkapp/feature-overview/permissions#example-impossible-to-upgrade and https://minafoundation.github.io/mina-fungible-token/deploy.html) */
allowUpdates: boolean
}

export const FungibleTokenErrors = {
Expand Down Expand Up @@ -72,17 +75,22 @@ export class FungibleToken extends TokenContract {

this.account.permissions.set({
...Permissions.default(),
setVerificationKey: Permissions.VerificationKey.impossibleDuringCurrentVersion(),
setVerificationKey: props.allowUpdates
? Permissions.VerificationKey.proofDuringCurrentVersion()
: Permissions.VerificationKey.impossibleDuringCurrentVersion(),
setPermissions: Permissions.impossible(),
access: Permissions.proof(),
})
}

/** Update the verification key.
* Note that because we have set the permissions for setting the verification key to `impossibleDuringCurrentVersion()`, this will only be possible in case of a protocol update that requires an update.
* This will only work when `allowUpdates` has been set to `true` during deployment.
*/
@method
async updateVerificationKey(vk: VerificationKey) {
const adminContract = await this.getAdminContract()
const canChangeVerificationKey = await adminContract.canChangeVerificationKey(vk)
canChangeVerificationKey.assertTrue(FungibleTokenErrors.noPermissionToChangeAdmin)
this.account.verificationKey.set(vk)
}

Expand Down
9 changes: 9 additions & 0 deletions FungibleTokenAdmin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type FungibleTokenAdminBase = SmartContract & {
canChangeAdmin(admin: PublicKey): Promise<Bool>
canPause(): Promise<Bool>
canResume(): Promise<Bool>
canChangeVerificationKey(vk: VerificationKey): Promise<Bool>
}

export interface FungibleTokenAdminDeployProps extends Exclude<DeployArgs, undefined> {
Expand Down Expand Up @@ -87,4 +88,12 @@ export class FungibleTokenAdmin extends SmartContract implements FungibleTokenAd
await this.ensureAdminSignature()
return Bool(true)
}

@method.returns(Bool)
public async canChangeVerificationKey(
_vk: VerificationKey,
): Promise<Bool> {
await this.ensureAdminSignature()
return Bool(true)
}
}
38 changes: 20 additions & 18 deletions documentation/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,24 @@ to see executable end to end example.

## A Note on Upgradeability

By default, the token and admin contract have permissions set so that they cannot be upgraded,
except in case of a non-backwards compatible hard fork of Mina (see
Upgradeability of smart contracts is a double edged sword: on one hand, it allows you to fix errors,
improve performance, and stay up to date with third party libraries (such as o1js). But on the other
hand, the possibility of arbitrary code changes during a redeploy places an enormous amount of trust
in the deployer.

In Mina, upgradeability is determined via the permissions of the account that the contract is
deployed to. One possibility is to only allow contract upgrades when there has been a breaking
change in the protocol itself (see
[Mina documentation on upgradeability](https://docs.minaprotocol.com/zkapps/writing-a-zkapp/feature-overview/permissions#example-impossible-to-upgrade)).
This is to ensure that the rules around the token are not changed after the token is deployed.

Depending on the maturity of your project, that might or might not be what you want. Disallowing
contract updates gives a strong guarantee to token holders that the rules around the token will not
change. For example, if the admin contract ensures a limited supply of the token, or forbids minting
new tokens after a certain date, then a change of that contract can undo those guarantees. This
might be the right thing to do if you have figured out exactly how you want the token to behave, and
are sure that you will not make any changes in the future.

If you do want to reserve the possibility to make changes in the future, then you should alter the
`deploy()` functions to set the account permissions for the token and admin contracts to allow
changes to the verification key. If you do that, you will probably also want to allow future changes
to the account permissions, to eventually disallow further changes to the verification key.

If you are looking to acquire fungible tokens, you should consider that if the deployer used more
permissive account permissions for the contracts, they might change the contracts in the future.
This was the default behaviour in the original release of the token contract (v1.0.0).

However, this did not allow updating the contract in order to stay up to date with new versions of
the o1js library -- which can be desirable, for example to include bug fixes or performance
improvements.

In order to allow updates, there is now an option to allow updates of the contract, by setting
`allowUpdates` to `true` when calling `deploy()`. This is recommended, in order to allow updating
the token contract when there is a new version of o1js. The downside is that this does require token
holders to trust the token admin to not make arbitrary changes to the contract. In order to lower
the amount of trust needed, we are planning to use a more refined access control (using multi-sig)
in an upcoming version of the token standard.
1 change: 1 addition & 0 deletions examples/concurrent-transfer.eg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const deployTx = await Mina.transaction({
await token.deploy({
symbol: "abc",
src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
allowUpdates: true,
})
await token.initialize(
admin.publicKey,
Expand Down
1 change: 1 addition & 0 deletions examples/e2e.eg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const deployTx = await Mina.transaction({
await token.deploy({
symbol: "abc",
src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
allowUpdates: true,
})
await token.initialize(
admin.publicKey,
Expand Down
1 change: 1 addition & 0 deletions examples/escrow.eg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const deployTokenTx = await Mina.transaction({
await token.deploy({
symbol: "abc",
src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
allowUpdates: true,
})
await token.initialize(
admin.publicKey,
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@
"typescript": "^5.4.3"
},
"engines": {
"node": ">=18.14.0"
"node": ">=20.0.0"
}
}
Loading