From c035d5550c53094200185a83d71693afd1be4950 Mon Sep 17 00:00:00 2001 From: Philipp Kant Date: Tue, 10 Dec 2024 17:47:21 +0100 Subject: [PATCH 1/3] Allow updating the verification key. This PR does two things: - it adds a method `canChangeVerificationKey` to the admin contract, to check permissions for changing the verification key. This method is called from `updateVerificationKey` in the token contract. - The deploy arguments for the token contract now have a new field, `allowUpdates`. If that is set to `true`, updates of the verification key will be allowed also during the current protocol version. This allows updating the token contract, for example when there is a new version of o1js. --- FungibleToken.test.ts | 73 ++++++++++++++++++++++++++++++ FungibleToken.ts | 12 ++++- FungibleTokenAdmin.ts | 9 ++++ examples/concurrent-transfer.eg.ts | 1 + examples/e2e.eg.ts | 1 + examples/escrow.eg.ts | 1 + 6 files changed, 95 insertions(+), 2 deletions(-) diff --git a/FungibleToken.test.ts b/FungibleToken.test.ts index 873a58c..0410ba0 100644 --- a/FungibleToken.test.ts +++ b/FungibleToken.test.ts @@ -15,6 +15,7 @@ import { state, UInt64, UInt8, + VerificationKey, } from "o1js" import { FungibleToken, @@ -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() @@ -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, @@ -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, @@ -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, @@ -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, @@ -936,6 +1003,12 @@ class CustomTokenAdmin extends SmartContract implements FungibleTokenAdminBase { this.ensureAdminSignature() return Bool(true) } + + @method.returns(Bool) + public async canChangeVerificationKey(_vk: VerificationKey): Promise { + this.ensureAdminSignature() + return Bool(true) + } } export default class ThirdParty extends SmartContract { diff --git a/FungibleToken.ts b/FungibleToken.ts index 3d4f0bb..f7bb3da 100644 --- a/FungibleToken.ts +++ b/FungibleToken.ts @@ -27,6 +27,9 @@ interface FungibleTokenDeployProps extends Exclude { /** 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 = { @@ -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) } diff --git a/FungibleTokenAdmin.ts b/FungibleTokenAdmin.ts index e2e700a..cbacc3d 100644 --- a/FungibleTokenAdmin.ts +++ b/FungibleTokenAdmin.ts @@ -18,6 +18,7 @@ export type FungibleTokenAdminBase = SmartContract & { canChangeAdmin(admin: PublicKey): Promise canPause(): Promise canResume(): Promise + canChangeVerificationKey(vk: VerificationKey): Promise } export interface FungibleTokenAdminDeployProps extends Exclude { @@ -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 { + await this.ensureAdminSignature() + return Bool(true) + } } diff --git a/examples/concurrent-transfer.eg.ts b/examples/concurrent-transfer.eg.ts index eb749d2..6835453 100644 --- a/examples/concurrent-transfer.eg.ts +++ b/examples/concurrent-transfer.eg.ts @@ -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, diff --git a/examples/e2e.eg.ts b/examples/e2e.eg.ts index 38a8faf..e51bc7d 100644 --- a/examples/e2e.eg.ts +++ b/examples/e2e.eg.ts @@ -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, diff --git a/examples/escrow.eg.ts b/examples/escrow.eg.ts index 5ef1cc2..2f077b2 100644 --- a/examples/escrow.eg.ts +++ b/examples/escrow.eg.ts @@ -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, From 724aae8102c245a90129adaafc3b64ab00e06d0a Mon Sep 17 00:00:00 2001 From: Philipp Kant Date: Tue, 10 Dec 2024 18:03:18 +0100 Subject: [PATCH 2/3] Document the changes to updateability --- documentation/deploy.md | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/documentation/deploy.md b/documentation/deploy.md index ade5e4f..0158d2b 100644 --- a/documentation/deploy.md +++ b/documentation/deploy.md @@ -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. From bf4ff800a81e83711912fc48c2cc8f77db7a624a Mon Sep 17 00:00:00 2001 From: Philipp Kant Date: Tue, 10 Dec 2024 18:04:58 +0100 Subject: [PATCH 3/3] Require node >=20.0.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4849b5f..951e8d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "typescript": "^5.4.3" }, "engines": { - "node": ">=18.14.0" + "node": ">=20.0.0" }, "peerDependencies": { "o1js": "^2.1.0" diff --git a/package.json b/package.json index db637fa..eae0301 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,6 @@ "typescript": "^5.4.3" }, "engines": { - "node": ">=18.14.0" + "node": ">=20.0.0" } }