Deploy your first smart contract using pint

"counter" is one of the simplest smart contracts that can be written in Pint. It showcases how a contract can have multiple predicates and how it can declare and use storage

Simple Counter contract

Create a new file named counter.pnt.

storage {
    counter: int,
}

predicate Initialize(value: int) {
    let counter: int = mut storage::counter;
    constraint counter' == value;
}

predicate Increment(amount: int) {
    let counter: int = mut storage::counter;
    constraint counter' == counter + amount;
}

The contract starts by declaring a storage block which contains a single storage variable called counter of type int (i.e. integer). The contract later declares two separate predicates, each having a single parameter and declaring two statements. Let's walk through the first predicate named Initialize:

  1. Predicate Initialize has a single parameter called value of type int. The parameters of a predicate are essentially decision variables that a solver is required to find values for such that every constraint in the predicate evaluates to true. The expression "decision variable" is commonly used in constraint programming languages to refer to unknowns that a solver must find given some constraints. In Initialize, the parameter value is the value that we want our counter to get initialized to.

  2. The second statement declares a local variable and initializes it to mut storage::counter. The statement let counter: int = mut storage::counter creates a local variable called counter and initializes it to the current value of counter declared in the storage block. The mut keyword simply indicates that the solver is allowed to propose a new value for counter. If mut is not present, then the storage variable counter cannot be modified by anyone attempting to solve predicate Initialize.

  3. The third statement contains the core logic of this predicate. It declares that the "next value" of counter must be equal to value. Note the ' notation here which can be only applied to a local variable, and means "the next value of the state variable after a valid state transition".

The second predicate, called Increment, has a similar structure to Initialize. However, instead of initializing counter, It increments it by amount. Note that both counter (the current value) and counter' (the next value) are both used in the constraint to enforce that the next value is dependent on the current value, which was not the case in Initialize.

Deploy a token smart contract

In this example you will get a clear understanding on how to create a simple token smart contract using pint, compile and deploy it on a local network. The components are as follows 1) Storage Struct

use std::lib::PredicateAddress;
use std::lib::@delta;
use std::lib::@safe_increment;
use std::lib::@init_once;
use std::lib::@init_delta;
use std::lib::@mut_keys;
use std::auth::@verify_key;
use std::lib::Secp256k1Signature;

storage {
    balances: (b256 => int),
    nonce: (b256 => int),
    token_name: b256,
    token_symbol: b256,
    decimals: int,
}
  • Storage Definitions:

    • balances: A mapping of account addresses (b256) to their token balances (int).

    • nonce: Tracks the number of transactions made by each account to safeguard against replay attacks.

    • token_name: A hashed representation of the token's name.

    • token_symbol: A hashed representation of the token's symbol.

    • decimals: An integer indicating the token's decimal precision.

interface BurnAccount {
    predicate Owner(
        key: b256,
        amount: int,
        token_address: PredicateAddress,
    );
}

BurnAccount Interface:

  • Contains a predicate Owner which verifies authorization to burn tokens. It requires:

    • key: The account address.

    • amount: The number of tokens to burn.

    • token_address: The address of the predicate used for verification purposes.

macro @check_if_predicate_is_owner($c, $p, $address, $arg0) {
    $c@[$address.contract]::$p@[$address.addr]($arg0, { contract: __this_contract_address(), addr: __this_address() })
}

macro @check_if_predicate_is_owner($c, $p, $address, $arg0, $arg1) {
    $c@[$address.contract]::$p@[$address.addr]($arg0, $arg1, { contract: __this_contract_address(), addr: __this_address() })
}

macro @check_if_predicate_is_owner($c, $p, $address, $arg0, $arg1, $arg2) {
    $c@[$address.contract]::$p@[$address.addr]($arg0, $arg1, $arg2, { contract: __this_contract_address(), addr: __this_address() })
}
  • Macros:

    • Macros @check_if_predicate_is_owner are defined to streamline the process of checking if a given predicate is the owner. These macros allow for different numbers of arguments:

      • They evaluate whether a specified contract and predicate address match, utilizing the current contract and its address.

      • The macros can accept varying numbers of arguments (from one to three), enhancing flexibility. These arguments assist in conducting ownership checks in different contexts of authorization.

      These elements work together to ensure secure execution of token-burning functions, while also enabling efficient ownership verification processes.

union BurnAuth = Signed(Secp256k1Signature) | Predicate(PredicateAddress);
union MintAuth = Signed(Secp256k1Signature) | Predicate(PredicateAddress);
union TransferSignedMode = All | Key | KeyTo | KeyAmount;
type TransferSignedAuth = { sig: Secp256k1Signature, mode: TransferSignedMode };
union TransferAuthMode = Signed(TransferSignedAuth) | Predicate(PredicateAddress);
type Extra = { addr: PredicateAddress };
union ExtraConstraints = Extra(Extra) | None;
type TransferAuth = { mode: TransferAuthMode, extra: ExtraConstraints };
  • BurnAuth & MintAuth: These are union types that determine the authorization method for burning and minting tokens. They can either be signed using a Secp256k1Signature or validated using a PredicateAddress.

  • TransferSignedMode: An enumeration defining the different modes of signed transfer authorization including options such as All, Key, KeyTo, and KeyAmount.

  • TransferSignedAuth: A type that combines a Secp256k1Signature and a TransferSignedMode, representing signed authorization for transfers.

  • TransferAuthMode: A union that encapsulates the method of transfer authorization, accepting either a signed authorization or a predicate-based one.

  • Extra: A type holding a PredicateAddress, providing additional information for authorization.

  • ExtraConstraints: A union type that can either be an Extra or None, indicating if there are additional constraints on authorization.

  • TransferAuth: A type representing the complete transfer authorization details, combining TransferAuthMode and ExtraConstraints for comprehensive transfer verification.

interface MintAccount {
    predicate Owner(
        key: b256,
        amount: int,
        decimals: int,
        token_address: PredicateAddress,
    );
}


predicate Mint(key: b256, amount: int, decimals: int, auth: MintAuth) {
    let balance = mut storage::balances[key];
    let nonce = mut storage::nonce[key];
    let token_name = mut storage::token_name;
    let token_symbol = mut storage::token_symbol;
    let token_decimals = mut storage::decimals;

    constraint key == config::MINT_KEY;
    constraint @init_once(balance; amount);
    constraint @init_once(token_name; config::NAME);
    constraint @init_once(token_symbol; config::SYMBOL);
    constraint @init_once(token_decimals; decimals);
    constraint @init_once(nonce; 1);

    constraint match auth {
        MintAuth::Signed(sig) => @verify_key({key, amount, decimals, nonce'}; sig; key),
        MintAuth::Predicate(addr) => @check_if_predicate_is_owner(MintAccount; Owner; addr; key; decimals; amount),
    };
}

The interface MintAccount defines a predicate called Owner to verify ownership when minting new tokens. It requires certain parameters: a key (b256), an amount (int), decimals (int), and a token address (PredicateAddress). This predicate ensures that the entity attempting to mint tokens has legitimate ownership and the right parameters.

The predicate Mint function checks if tokens can be minted under specific conditions. It ensures:

  • The key matches a predefined MINT_KEY.

  • Various token attributes like balance, token_name, and token_symbol are initialized correctly.

  • The auth parameter is validated either through a signature or by checking if a predicate is the owner, using the @check_if_predicate_is_owner macro.

This setup secures the minting process, ensuring only authorized entities can mint tokens by verifying ownership through signatures or predicates.

predicate Transfer(key: b256, to: b256, amount: int, auth: TransferAuth) {
  {
    let sender_balance = mut storage::balances[key];
    let receiver_balance = mut storage::balances[to];
    let nonce = mut storage::nonce[key];
    constraint amount > 0;
    constraint sender_balance' >= 0;
    constraint @delta(sender_balance) == 0 - amount;.
    constraint @init_delta(receiver_balance; amount);
    constraint @safe_increment(nonce);
    
   
    constraint match auth.mode {
        TransferAuthMode::Signed(auth) => match auth.mode {
            TransferSignedMode::All => @verify_key({key, to, amount, nonce'}; auth.sig; key),
            TransferSignedMode::Key => @verify_key({key, nonce'}; auth.sig; key),
            TransferSignedMode::KeyTo => @verify_key({key, to, nonce'}; auth.sig; key),
            TransferSignedMode::KeyAmount => @verify_key({key, amount, nonce'}; auth.sig; key),
        },
        TransferAuthMode::Predicate(addr) => @check_if_predicate_is_owner(TransferAccount; Owner; addr; key; to; amount),
    };
    constraint match auth.extra {
        ExtraConstraints::Extra(extra) => ExtraConstraintsI@[extra.addr.contract]::Check@[extra.addr.addr]({ contract: __this_contract_address(), addr: __this_address() }),
        ExtraConstraints::None => true,
    };
    }
    

The Transfer predicate validates a token transfer between two addresses in a smart contract. It checks several conditions:

  1. Balance checks: The sender must have enough balance, and the receiver's balance is updated accordingly.

  2. Amount validation: The transfer amount must be positive.

  3. Nonce management: The sender’s nonce is safely incremented to prevent replay attacks.

  4. Authentication: The transaction is authorized either by a digital signature (with various verification modes) or by checking a predicate to verify ownership.

  5. Additional constraints: If there are any extra constraints (provided in auth.extra), they are validated.

Last updated