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 tosend
, thereceive
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. ThezkappUri
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. ThetokenSymbol
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. ThevotingFor
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 tonone
- and therefore can manipulate the fields as they please.impossible
: If a field permission is set toimpossible
, nothing can ever change this field!signature
: Fields that have their permission set tosignature
can only be manipulated by account updates that are accompanied and authorized by a valid signature.proof
: Fields that have their permission set toproof
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 toproofOrSignature
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 example provided in simple_zkapp_payment.ts 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 accountdeposit
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 examples.