Smart Contracts

Pint is a language for writing "smart contracts". If you're familiar with smart contract languages like Solidity, then many elements of a Pint contract should feel familiar. Of course, at its core, Pint is fundamentally different from imperative smart contract languages. Writing smart contracts in Pint requires a different way of thinking about how to express the rules that the smart contract must enforce.

A Pint contract is a collection of predicates. Each predicate has a name, a list of typed parameters, and a list of constraints. Predicates of a smart contract describe the various ways state can be mutated in order to accomplish certain tasks (e.g. token transfer). A contract may also contain a storage declaration which contain all the storage variable that the contract owns. Contract storage is effectively the database of the contract where persistent state lives. We will discuss storage in details in Chapter 5.

Contract Structure

The structure of a Pint smart contract is fairly simple. We simply lay out all the code blocks as follows:

storage {
    // storage variables
}

predicate Foo(
    // parameters 
) {
    // local variables, constraints, etc.     
}

predicate Bar(
    // parameters 
) {
    // local variables, constraints, etc.     
}

// other predicates

The order of the code blocks is not important, and the storage block is optional but most useful smart contracts will need it.

Unlike imperative smart contracts where the logic lives in contract methods that can be called to make state updates, Pint contracts have predicates (not methods/functions even though they look like ones!) and nothing is ever "called". Instead, solutions have to submitted that satisfy one or more predicates in the contract. A solution must specify concrete values for the parameters of the solved predicates, as well as propose changes to the state if necessary.

If the proposed values for the parameters and the proposed state changes satisfy all the constraints for each solved predicate (potentially including predicates from other contracts), then the solution is deemed valid and the proposed state changes should be accepted.

Predicate Parameters

A predicate parameter is a named parameter that every solution is required to assign a value for. Predicate parameters are quite different from "regular" function parameters that you might be used to in imperative programming languages since predicates of a smart contract are not called!. Instead, they are solved by proposing values for these parameters such that all the constraints are satisfied.

All predicate parameters have to be annotated with a type. We will go over the available data types in Pint, in detail, in a later chapter.

Here's an example that shows how to declare a predicate named test with three parameters foo, bar, and baz with three different types:

predicate test(
    foo: int, 
    bar: bool, 
    baz: b256, 
) {
    constraint foo * foo <= 1024;

    // other constraints
}

The predicate test also declares a constraint that enforces that the square of foo is at most 1024, meaning that any proposed solution must assign a value for foo that satisfies this condition. For example, if foo is set to 7, this is constraint would be satisfied. If foo is set to 42, this constraint would not be satisfied. We will go over constraints in more detail in Chapter 4.6.

You can think of the type annotation on each predicate parameter as an implicit "constraint". For example, parameter foo can only take values in the set of signed integers (64-bit signed integers when targeting the EssentialVM) while bar can only take two values: false or true (i.e. 0 or 1 in the EssentialVM).

Contract Interfaces

Each smart contract has an interface which can be easily generated from the contract. The interface is not required to write the smart contract but is required to interact with the contract from external contexts. For example, one smart contract can propose an update to a storage variables that is owned by another contract. Will will go over how to do that in Chapter 5. Another example is invoking external predicates which is a more advanced topic that we will cover in Chapter 7.1.

A contract interface has the following structure:

interface ExternalContract {
    storage {
        // storage variables
    }

    predicate Foo(
        //  parameters
    );

    predicate Bar(
        // parameters     
    );

    // other predicate interfaces 
}

You can see the similarities between the structure of the interface and the structure of the smart contract. An interface starts with interface keyword followed by the name of the interface which can be used when referring the interface from external contexts. Inside the interface declaration, an optional storage block can be declared as well as a list of predicate interfaces, each with its own list of typed parameters. The storage block and the predicate signatures of an interface should always match the corresponding storage block and predicate signatures of the deployed contract. Otherwise, correct behavior cannot be guaranteed.

For example, an interface for the counter example looks like this:

interface Counter {
    storage {
        counter: int,
    }

    predicate Initialize(value: int);

    predicate Increment(amount: int);
}

Hopefully nothing here is surprising! The key point is that an interface must expose everything that is public in a contract, and that includes the storage block and the predicate signatures.

Note in the future, Pint will have a tool that will auto-generate interfaces from a smat contract.