Documentation
Language Features
Declarative Permissions

Declarative Permissions

⚠️️

By default, all contracts are fully public . This is semantically equivalent to using the @public directive.

Polylang controls access (read/write) to data in contracts through its permissions model. This is achieved via the following directives - @public (default), @private, @read, @call, and @delegate, as well as secondary methods such as checking authorization (using a public key, for instance).

Polylang Access Control is best understood in terms of what the user is allowed to do (or not do). This leads to the following permission modes:

  1. Allow all

  2. Delegation

  3. Using custom code (for more granular controls)

Each of these modes is described in detail in the following sections.

Allow all

This is the least constrained mode of access to contracts data. This mode allows anyone to read from and write to (i.e., call Polylang functions which modify the record data) contracts in varying levels of granularity using directives.

This is the default mode of operation.

The following are equivalent:

contract MyContract {
    ...
}

and

@public
contract MyContact {
    ...
}

@read

This directive allows anyone to read contract data, but not to write data (invoke functions). This directive can be applied to both contracts as well as to a specific field (which must be of type PublicKey).

To signify read permission on the overall contract:

@read
@private
contract Person {
    id: string;
    name: string;
    age: number;
 
    constructor (id: string name: string, age: number) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
 
    setName(newName: string) {
        this.name = newName;
    }
 
    setAge(newAge: number) {
        this.age = newAge;
    }
 
    del () {
        selfdestruct();
    }
}

In this case, anyone can read Person data, but no one (including the user who created the Person contract) can invoke the setName, setAge, and del functions (since the contract is marked @private).

If we wish to enforce a stronger constraint that only the creator of the Person contract be allowed to read the contract, we can modify the contract like so:

@private
contract Person {
    id: string;
    name: string;
    age: number;
 
    @read
    creator: PublcKey;
 
    constructor (id: string name: string, age: number) {
        this.id = id;
        this.creator = ctx.publicKey;
        this.name = name;
        this.age = age;
    }
 
    setName(newName: string) {
        this.name = newName;
    }
 
    setAge(newAge: number) {
        this.age = newAge;
    }
 
    del () {
        selfdestruct();
    }
}

Since the contract above is marked as @private, no one other than the creator of the contract, i.e., the user whose public key matches the public key stored in the creator field can read the contract data.

@call

This directive allows anyone to invoke functions on the contract record (if the contract is marked @private), but not read data from the contract. This directive may be applied on contracts or on individual functions.

We will consider both cases using the same example.

Consider the following contract:

@call
@private
contract Person {
    id: string;
    name: string;
    age: number;
 
    // allows anyone to read this field
    @read
    creator: PublicKey;
 
    constructor (id: string name: string, age: number) {
        this.creator = ctx.publicKey;
        this.id = id;
        this.name = name;
        this.age = age;
    }
 
    setName(newName: string) {
        this.name = newName;
    }
 
    setAge(newAge: number) {
        this.age = newAge;
    }
 
    // this function can only be called by the user whose public keys matches the one in the `creator` field.
    @call(creator)
    del () {
        selfdestruct();
    }
}

In the example above, no one can read Person data, but anyone can invoke the functions setName and setAge, but not the del function (since it has a @call directive on it, which overrides the directive on the contract).

@private

This directive is the most restrictive of all, allowing no one (including the creator of the contract) to read from or write to the contract post creation. This directive can only be applied on contracts.

For example:

@private
contract Person {
    id: string;
    name: string;
    age: number;
 
    constructor (id: string name: string, age: number) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
 
    setName(newName: string) {
        this.name = newName;
    }
 
    setAge(newAge: number) {
        this.age = newAge;
    }
 
    del () {
        selfdestruct();
    }
}

Once the Person contract record has been created, no one can read its data (or update it).

Delegation

Delegation involves delegating or offloading the responsibility of checking for read/write permissions onto a field which is either a PublicKey, or an arbitrary long chain of fields of contract types which ultimately ends in a field of type PublicKey. Delegation makes use the @delegate directive coupled with @read and/or @call.

Suppose we wish to read data from the Response contract, we can specify that the read permisson check be handled by the form field (a Form contract), and Form in turns delegates the responsibility of checking read permissions to its creator field (or type User). Finally, the User contract has a publicKey field which handles the actual read permission checks.

So when a user tries to read from the Response contract, the read will only succeed if the publicKey field in the transitively associated contract, User matches that or the user attempting the operation.

Likewise, the approve function may only be called by a user if the delegation chain ends up confirming that the publicKey field in User is the same as that of the user attempting the function call operation. Additionally, note that the someOtheFunction in the Response contract cannot be called by anyone.

contract Response {
    // delegate read permission to the form field
    @read
    form: Form;
 
    @call(form)
    approve() {
        ...
    }
 
    someOtheFunction() {
        ...
    }
    ...
}
 
contract Form {
    // delegate read permission to the User field
    @delegate
    creator: User;
 
    ...
}
 
contract User {
    // delegate read permission to publicKey
    @delegate
    publicKey: PublicKey;
 
    ...
}
⚠️

Since the chain of delegates must end up on a field of type PublicKey, the delegation model requires the usage of public keys. This means that the user must be authenticated before attempting the operation. This is outside the purview of Polylang.

Using Custom Code

⚠️️

This mode only works for write access, not for read access.

While delegation provides an elegant way of controlling permissions in Polylang, for more complicated scenarios, checking permissions using custom code is often the most flexible approach. This can also turn out to be the easiest to understand in many cases.

The usual way is provide tighter constraints via custom checks. For example:

contract Team {
    id: string
    members: string[]
    publicKey: PublicKey;
 
    // Anyone can call this function because the contract is public by default
    addMember (id: string) {
      // But you must be signing using the correct key
      if (this.publicKey == ctx.publicKey) {
          throw error('invalid user');
      }
 
      // And there must not be already more than 5 members
      if (this.members.length > 5) {
          throw error('too many members');
      }
 
      // Now we have checked rules, we can write the data
      this.members.push(id);
    }
}

In the example above, anyone can read the data since the contract is public by default. However, for the addMember function which modifies data, we wish to have tighter constrainsts. As such, we have two checks in place - the first check ensures that the public key of the user invoking the function matches the public key stored in the contract during the creation of the record.

Secondly, we have a custom check that allows the operation to proceed only if we have enough members in the team. This second form of checks cannot be done using directives and/or delegates, and is domain-specific in nature.

The flexibility afforded by the Polylang permissions model allows the user to specify constraints depending on their domain of interest.

Summary of Permission modes

The various basic modes of permissions provided by the use of directives is summarised in the following table. Note that there are several more ways in which these modes can be combined to obtain varying granularities of permissions.

DirectiveEntityReadWrite
None / @publicContractAnyoneAnyone
@privateContractNo oneNo one
@readContractAnyoneNo one
@readfieldOnly user with matching public keyNo one
@callContractNo oneAnyone
@callfunctionNo oneOnly user with matching public key
@delegate + @readfunctionOnly user with a transitively matching public keyNo one
@delegate + @callfunctionNo oneOnly user with a transitively matching public key

Declarative Permissions and Proofs

During compilation, the Polylang compiler evaluates the AST (Abstract Syntax Tree) and selectively generates Miden VM assembly instructions based on the declarative permissions in the contract (such as the@call directive) by checking the various rules mentioned in the sections above.

When the program is run by the Polylang prover, the generated code is evaluated, run, and becomes part of the generated proof.


Polylang Docs