Skip to main content
info

Please note that zkApp programmability is not yet available on Mina Mainnet, but zkApps can now be deployed to Berkeley Testnet.

Permissions

Permissions are an integral part of zkApp development because they determine who has the authority to interact and make changes to a specific part of a smart contract. Naturally, every smart contract must have proper permissions to prevent attacks or security holes. Permissions live on-chain, which means they are a part of the account representation on the network. Permissions are checked every time an account update tries to interact with an account.

Types of Permissions

There are 11 different types of permissions that a developer can access and adjust to guard a zkApp account:

  • editState: The permission describing how the zkApp account's eight on-chain state fields are allowed to be manipulated. This permission describes how the on-chate state fields can be manipulated.

  • send: The permission corresponding to the ability to send transactions from this account. This permission determines whether someone can send a transaction, for example, transfer MINA from this particular account.

  • receive: Similar to send, the receive permission determines whether a particular account can receive transactions, for example, depositing MINA.

  • setDelegate: The permission corresponding to the ability to set the delegate field of the account. The delegate field is the address of another account to whom this account is delegating its MINA for staking.

  • setPermissions: The permission corresponding to the ability to change the permissions of the account. As the name suggests, this type of permission describes how already set permissions can be changed.

  • setVerificationKey: The permission corresponding to the ability to change the verification key of the account. Every smart contract has a verification key stored on-chain. The verification key is used to verify off-chain proofs. This permission essentially describes if the verification key can be changed; you can also think of it as the "upgradeability" of smart contracts.

  • setZkappUri: The permission corresponding to the ability to change the zkapp uri field of the account. The zkappUri field can be used to store metadata about the smart contract, for example, link to the source code.

  • editActionsState: The permission that corresponds to the ability to change the actions state of the associated account. Every smart contract can dispatch actions that are committed on-chain. This type of permission describes who can change the actions state.

  • setTokenSymbol: The permission corresponding to the ability to set the token symbol for this account. The tokenSymbol field stores the symbol of a token.

  • incrementNonce: The permission that determines whether to increment the nonce with an account update and who can increment the nonce on this account with a transaction.

  • setVotingFor: The permission corresponding to the ability to set the chain hash for this account. The votingFor field is an on-chain mechanism to set the chain hash of the hard fork this account is voting for.

  • access: This permission is more restrictive than all the other permissions combined! It corresponds to the ability to include any account update for this account in a transaction, even no-op account updates. Usually, the access permission is set to require no authorization. However, for token manager contracts, access requires at least proof authorization so that token interactions are approved by calling one of the token manager's methods.

  • setTiming: The permission corresponding to the ability to control the vesting schedule of time-locked accounts.

Authorization

Authorization determines what resources can be accessed, while permissions just describe who has the ability to execute an action.

A transaction consists of multiple account updates (sort of like instructions to the network) - and each account update must be authorized in one way or another. When you inspect an account update directly in SnarkyJS or using an explorer, you see the authorization field.

  • If the authorization field has a proof attached, it means the transaction is authorized by a proof that is checked against the verification key of the account.
  • If the authorization field has a signature, it means the account update is authorized by a signature.

Types of Authorizations

The types of authorizations are:

  • none: Everyone has access to fields with permission set to none - and therefore can manipulate the fields as they please.

  • impossible: If a field permission is set to impossible, nothing can ever change this field!

  • signature: Fields that have their permission set to signature can only be manipulated by account updates that are accompanied and authorized by a valid signature.

  • proof: Fields that have their permission set to proof can be manipulated only by account updates that are accompanied and authorized by a valid proof. Proofs are generated by proving the execution of a smart contract method. A proof is checked against the verification key of the account to ensure that state is changed only if the user generated a valid proof by executing a smart contract method correctly.

  • proofOrSignature: As the name might suggest, permissions with authorization set to proofOrSignature accept either a valid signature or a valid proof.

This example account update is authorized by a signature:

{
"authorization": {
"proof": null,
"signature": "7mXAcTFeybdZkFmYmfoRYRzVeMxQsGU5Uxq1RpRpGSkHEa5ZEraTRJ4cNKMnAS1n3NCmVqDnHUyraJs131dcdFi3sZH1Qzos"
},
// ...
"body": {
// ...
"update": {
"appState": [
"1",
"0",
"0",
"0",
"0",
"0",
"0",
"0"
],
// ...
},
// ...
}
}

As you can see, this example account update has an authorization with a signature provided. You can also see it's trying to update the app state of the smart contract.

However, imagine if a smart contract had the the permission editState set to the authorization none. When authorization is none, everyone can freely change the state of the smart contract as they please! Obviously, this is not a safe practice. To allow state changes only when a valid proof accompanies the account update that wants to access the state, set your authorization to proof so that the transaction is authorized by a proof that is checked against the verification key of the account. Using a proof authorization ensures that state is changed only if the user generated a valid proof by executing a smart contract method correctly.

Default Permissions

Smart contracts, when first deployed, always start with this default set of permissions:

editState: proof

send: proof

receive: none

setDelegate: signature

setPermissions: signature

setVerificationKey: signature

setZkappUri: signature

editActionsState: proof

setTokenSymbol: signature

Example

To better understand how to leverage permissions to make your smart contract more secure, look at these examples.

Sending MINA from smart contracts

Some smart contracts manage state and token, such as the native MINA token. To prevent malicious actors from withdrawing all funds, use permissions to secure them.

Consider the UnsecureContract smart contract. This example is also provided in the examples folder in the snarkyjs codebase repo:

class UnsecureContract extends SmartContract {
init() {
super.init();
this.account.permissions.set({
...Permissions.default(),
send: Permissions.none(),
});
}

@method deposit(amount: UInt64) {
let senderUpdate = AccountUpdate.createSigned(this.sender);
senderUpdate.send({ to: this, amount });
}
}

This UnsecureContract smart contract has two methods: withdraw for withdrawing funds from the smart contract account and deposit for depositing funds into the smart contract account. Notice that the permissions specified in the init method have set the send permission to Permissions.none(). Because none means you don't have to provide any form of authorization, a malicious actor can easily drain all funds from the smart contract! Take a look at the following malicious transaction that abuses this permission:

tx = await Mina.transaction(account1Address, () => {
let withdrawal = AccountUpdate.create(zkappAddress);
withdrawal.send({ to: account1Address, amount: 1e9 });
});
await tx.sign([account1Key]).send();

This malicious example transaction creates a new account update for our smart contract address. Right after that, the new account update sends 1 MINA to the address of the fee payer (account1Address). At the end of the transaction block, the transaction is signed only with the private key of the fee payer -- not the private key of the smart contract! Because the permissions for sending funds away from a smart contract are set to none, this transaction succeeds and drains 1 MINA from the smart contract.

Now, if you change the send permission to something else, like signature, the manual account update fails with Update_not_permitted_balance that prevents withdrawing funds from the smart contract since the authorization does not fit the permission for send that now requires a valid signature.

You can slightly modify the withdraw-transaction to include a valid signature by adding requesting a signature on the withdrawal account update and providing the private key of the smart contract account to tx.send([zkappKey])

tx = await Mina.transaction(account1Address, () => {
let withdrawal = AccountUpdate.create(zkappAddress);
withdrawal.send({ to: account1Address, amount: 1e9 });
withdrawal.requireSignature();
});
await tx.sign([account1Key, zkappKey]).send();

Now that you have provided a valid signature and the smart contract requires a valid signatures for sending funds from the smart contract, the transaction succeeds!

Upgradeability of smart contracts

Another important part of smart contract development is upgradeability. By using permissions, you can make our smart contract not upgradeable at all or upgradeable! On Mina, when you deploy a smart contract you generate a verification key from the contract source code. The verification key and the smart contract are stored on-chain and used to verify proofs that belong to that smart contract. Remember the permission called setVerificationKey? You can modify the authorization for this permission to set the upgradability of the smart contract.

Example: Impossible to upgrade

This simple example ensures that the smart contract is not upgradeable. After a verification key is deployed, it cannot be changed.

class UpgradeabilityImpossible extends SmartContract {
init() {
super.init();
this.account.permissions.set({
...Permissions.default(),
setVerificationKey: Permissions.impossible(),
});
}

@method updateVerificationKey(vk: VerificationKey) {
this.account.verificationKey.set(vk);
}
}

The UpgradeabilityImpossible smart contract has only one method: updateVerificationKey. By invoking this method and providing a new verification key, the verification key on-chain is expected to change. But since setVerificationKey permission is specified to be impossible, invoking that method fails - essentially making the smart contract not upgradeable.

console.log('try upgrading vk');
tx = await Mina.transaction(feePayer, () => {
zkapp.updateVerificationKey(newVerificationKey);
});
await tx.prove();
await tx.sign([feePayerKey, zkappKey]).send();

This transaction tries to replace the existing verification key with a new one, newVerificationKey. To can prevent that, set the permissions for a verification key change to impossible so the transaction fails.

Using the LocalBlockchain, you get the following (expected) error:

Error: Transaction verification failed: Cannot update field 'verificationKey' because permission for this field is 'Impossible'

For the sake of security, it is important to note that you must also set the field setPermissions to impossible to make the smart contract truly impossible to upgrade. This permission prevents a zkApp developer from changing the permission setVerificationKey to, for example, signature - which allows an actor to manipulate the verification key again.

Example: Upgradeable with a proof

There are situations where you might want the smart contract to be upgradeable. Modify the method updateVerificationKey to do some checks before you can update the verification key (as the result of a vote or other conditions). For now, just check that you can provide an x that is greater than or equal to 5. If this check succeeds, then update the verification key.

@method updateVerificationKey(vk: VerificationKey, x: Field) {
let y = Field(5);
x.gte(y).assertTrue();

this.account.verificationKey.set(vk);
}

You must update the permissions from setVerificationKey: impossible to proof because you want to change the verification key only if a valid proof is provided.

this.account.permissions.set({
...Permissions.default(),
setVerificationKey: Permissions.proof(),
});

Now when you invoke the updateVerificationKey method, the transaction generates a valid smart contract execution proof to succeed and upgrade the verification key to a new one!

console.log('try upgrading vk');
tx = await Mina.transaction(feePayer, () => {
zkapp.updateVerificationKey(newVerificationKey);
});
await tx.prove();
await tx.sign([feePayerKey, zkappKey]).send();

Where to learn more

Integration tests exercise the behavior of different permissions, including upgradeability. If you want to take a look, check out the voting integration test as well as the DEX integration test.