Examples#

FA1.2#

from dataclasses import dataclass
from pymich.michelson_types import *


@dataclass(eq=True, frozen=True)
class AllowanceKey(Record):
    owner: Address
    spender: Address


@dataclass(kw_only=True)
class FA12(BaseContract):
    tokens: BigMap[Address, Nat]
    allowances: BigMap[AllowanceKey, Nat]
    total_supply: Nat
    owner: Address

    def mint(self, _to: Address, value: Nat) -> None:
        if Tezos.sender != self.owner:
            raise Exception("Only owner can mint")

        self.total_supply = self.total_supply + value

        self.tokens[_to] = self.tokens.get(_to, Nat(0)) + value

    def approve(self, spender: Address, value: Nat) -> None:
        allowance_key = AllowanceKey(Tezos.sender, spender)

        previous_value = self.allowances.get(allowance_key, Nat(0))

        if previous_value > Nat(0) and value > Nat(0):
            raise Exception("UnsafeAllowanceChange")

        self.allowances[allowance_key] = value

    def transfer(self, _from: Address, _to: Address, value: Nat) -> None:
        if Tezos.sender != _from:
            allowance_key = AllowanceKey(_from, Tezos.sender)

            authorized_value = self.allowances.get(allowance_key, Nat(0))

            if (authorized_value - value) < Int(0):
                raise Exception("NotEnoughAllowance")

            self.allowances[allowance_key] = abs(authorized_value - value)

        from_balance = self.tokens.get(_from, Nat(0))

        if (from_balance - value) < Int(0):
            raise Exception("NotEnoughBalance")

        self.tokens[_from] = abs(from_balance - value)

        to_balance = self.tokens.get(_to, Nat(0))

        self.tokens[_to] = to_balance + value

    def getAllowance(self, owner: Address, spender: Address) -> Nat:
        return self.allowances.get(AllowanceKey(owner, spender), Nat(0))

    def getBalance(self, owner: Address) -> Nat:
        return self.tokens.get(owner, Nat(0))

    def getTotalSupply(self) -> Nat:
        return self.total_supply

FA2 multi-asset#

from dataclasses import dataclass
from pymich.michelson_types import *


@dataclass#(eq=True, frozen=True)
class OperatorKey(Record):
    owner: Address
    operator: Address
    token_id: Nat

@dataclass#(eq=True, frozen=True)
class LedgerKey(Record):
    owner: Address
    token_id: Nat

@dataclass#(eq=True, frozen=True)
class TokenMetadata(Record):
    token_id: Nat
    token_info: Map[String, Bytes]

@dataclass
class TransactionInfo(Record):
    to_: Address
    token_id: Nat
    amount: Nat

@dataclass
class TransferArgs(Record):
    from_: Address
    txs: List[TransactionInfo]


def require_owner(owner: Address) -> Nat:
    if Tezos.sender != owner:
        raise Exception("FA2_NOT_CONTRACT_ADMINISTRATOR")

    return Nat(0)


@dataclass(kw_only=True)
class FA2(BaseContract):
    ledger: BigMap[LedgerKey, Nat]
    operators: BigMap[OperatorKey, Nat]
    token_total_supply: BigMap[Nat, Nat]
    token_metadata: BigMap[Nat, TokenMetadata]
    owner: Address

    def create_token(self, metadata: TokenMetadata) -> None:
        require_owner(self.owner)

        new_token_id = metadata.token_id

        if new_token_id in self.token_metadata:
            raise Exception("FA2_DUP_TOKEN_ID")
        else:
            self.token_metadata[new_token_id] = metadata
            self.token_total_supply[new_token_id] = Nat(0)

    def mint_tokens(self, owner: Address, token_id: Nat, amount: Nat) -> None:
        require_owner(self.owner)

        if not token_id in self.token_metadata:
            raise Exception("FA2_TOKEN_DOES_NOT_EXIST")

        ledger_key = LedgerKey(owner, token_id)
        self.ledger[ledger_key] = self.ledger.get(ledger_key, Nat(0)) + amount
        self.token_total_supply[token_id] = self.token_total_supply[token_id] + amount

    def transfer(self, transactions: List[TransferArgs]) -> None:
        for transaction in transactions:
            for tx in transaction.txs:
                if not tx.token_id in self.token_metadata:
                    raise Exception("FA2_TOKEN_UNDEFINED")
                else:
                    if not (transaction.from_ == Tezos.sender or OperatorKey(transaction.from_, Tezos.sender, tx.token_id) in self.operators):
                        raise Exception("FA2_NOT_OPERATOR")

                    from_key = LedgerKey(transaction.from_, tx.token_id)
                    from_balance = self.ledger.get(from_key, Nat(0))

                    if tx.amount > from_balance:
                        raise Exception("FA2_INSUFFICIENT_BALANCE")

                    self.ledger[from_key] = abs(from_balance - tx.amount)

                    to_key = LedgerKey(tx.to_, tx.token_id)
                    self.ledger[to_key] = self.ledger.get(to_key, Nat(0)) + tx.amount

    def updateOperator(self, owner: Address, operator: Address, token_id: Nat, add_operator: Boolean) -> None:
        if Tezos.sender != owner:
            raise Exception("FA2_NOT_OWNER")

        operator_key = OperatorKey(owner, operator, token_id)
        if add_operator:
            self.operators[operator_key] = Nat(0)
        #else:
        #    del self.operators[operator_key]

    def balanceOf(self, owner: Address, token_id: Nat) -> Nat:
        if not token_id in self.token_metadata:
            raise Exception("FA2_TOKEN_UNDEFINED")

        return self.ledger.get(LedgerKey(owner, token_id), Nat(0))

Auction#

from pymich.michelson_types import *


class Auction(BaseContract):
    owner: Address
    top_bidder: Address
    bids: BigMap[Address, Mutez]

    def bid(self) -> None:
        if Tezos.sender in self.bids:
            raise Exception("You have already made a bid")

        self.bids[Tezos.sender] = Tezos.amount
        if Tezos.amount > self.bids[self.top_bidder]:
            self.top_bidder = Tezos.sender

    def collectTopBid(self) -> None:
        if Tezos.sender != self.owner:
            raise Exception("Only the owner can collect the top bid")

        self.ops = self.ops.push(
            Contract[Unit](Tezos.sender),
            self.bids[self.top_bidder],
            Unit(),
        )

    def claim(self) -> None:
        if not (Tezos.sender in self.bids):
            raise Exception("You have not made any bids!")

        if Tezos.sender == self.top_bidder:
            raise Exception("You won!")

        self.ops = self.ops.push(
            Contract[Unit](Tezos.sender),
            self.bids[Tezos.sender],
            Unit(),
        )

Decentralized Exchange#

# Pymich implementation of https://gitlab.com/dexter2tz/dexter2tz/-/blob/master/dexter.mligo
# Also uses flat curve from https://github.com/tezos-checker/flat-cfmm/blob/master/flat_cfmm.mligo

from dataclasses import dataclass
from pymich.michelson_types import *


PRICE_NUM = Nat(1)
PRICE_DENOM = Nat(1)

AMM = Nat(0)
CFMM = Nat(1)


def mutez_to_natural(a: Mutez) -> Nat:
    return a // Mutez(1)


def natural_to_mutez(a: Nat) -> Mutez:
    return a * Mutez(1)


def ceildiv(numerator: Nat, denominator: Nat) -> Nat:
    res = Nat(0)
    if denominator == Nat(0):
        raise Exception("DIV by 0")
    else:
        q = numerator // denominator
        r = numerator % denominator
        if r == Nat(0):
            res = q
        else:
            res = q + Nat(1)
    return res


@dataclass
class TransferParam(Record):
    _from: Address
    _to: Address
    value: Nat


@dataclass
class MintOrBurn(Record):
    quantity: Int
    target: Address


def token_transfer(
        ops: Operations,
        token_address: Address,
        from_: Address,
        to: Address,
        token_amount: Nat,
    ) -> Operations:
    transfer_entrypoint = Contract[TransferParam](token_address, "%transfer")
    return ops.push(
        transfer_entrypoint,
        Mutez(0),
        TransferParam(from_, to, token_amount),
    )


def mint_or_burn(
        ops: Operations,
        lqt_address: Address,
        target: Address,
        quantity: Int,
    ) -> Operations:
    mint_or_burn_entrypoint = Contract[MintOrBurn](lqt_address, "%transfer")
    return ops.push(
        mint_or_burn_entrypoint,
        Mutez(0),
        MintOrBurn(quantity, target),
    )


def xtz_transfer(
        ops: Operations,
        to: Address,
        amount: Mutez,
    ) -> Operations:
    return ops.push(
        Contract[Unit](to),
        amount,
        Unit(),
    )


def amm_tokens_bought(pool_in: Nat, pool_out: Nat, tokens_sold: Nat) -> Nat:
    return tokens_sold * Nat(997) * pool_out // (pool_in * Nat(1000) + (tokens_sold * Nat(997)))

@dataclass
class Point:
    x: Nat
    y: Nat


@dataclass
class SlopeInfo:
    """
    Gives information relative to the slope of a 2D curve

    :param [x]: x coodinate at which the slope is calculated
    :param [dx_dy]: derivative of x with respect to y
    """
    x: Nat
    dx_dy: Nat


def util(p: Point) -> SlopeInfo:
    plus = p.x + p.y
    minus = p.x - p.y
    plus_2 = plus * plus
    plus_4 = plus_2 * plus_2
    plus_8 = plus_4 * plus_4
    plus_7 = plus_8 // plus
    minus_2 = minus * minus
    minus_4 = minus_2 * minus_2
    minus_8 = minus_4 * minus_4
    minus_7 = Int(0)
    if minus != Int(0):
        minus_7 = minus_8 // minus

    return SlopeInfo(
        abs(plus_8 - minus_8),
        Nat(8) * abs(minus_7 + plus_7),
    )


@dataclass
class NewtonParam:
    x: Nat
    y: Nat
    dx: Nat
    dy: Nat
    u: Nat


def newton(param: NewtonParam) -> Nat:
    iterations = List[Nat](
        Nat(1), Nat(2), Nat(3), Nat(4),
    )
    for i in iterations:
        slope = util(Point(param.x + param.dx, abs(param.y - param.dy)))
        new_u = slope.x
        new_du_dy = slope.dx_dy
        param.dy = param.dy + abs((new_u - param.u) // new_du_dy)
    return param.dy


@dataclass
class FlatSwapParam:
    pool_in: Nat
    pool_out: Nat
    tokens_sold: Nat


def cfmm_tokens_bought(param: FlatSwapParam) -> Nat:
    x = param.pool_in * PRICE_NUM
    y = param.pool_out * PRICE_DENOM
    slope = util(Point(param.pool_in, param.pool_out))
    u = slope.x
    newton_param = NewtonParam(
        x,
        y,
        param.tokens_sold * PRICE_NUM,
        Nat(0),
        u,
    )
    return newton(newton_param)

def cfmm_xtz_bought(param: FlatSwapParam) -> Nat:
    x = param.pool_in * PRICE_DENOM
    y = param.pool_out * PRICE_NUM
    slope = util(Point(param.pool_in, param.pool_out))
    u = slope.x
    newton_param = NewtonParam(
        x,
        y,
        param.tokens_sold * PRICE_DENOM,
        Nat(0),
        u,
    )
    return newton(newton_param)


def get_tokens_bought(curve_id: Nat, xtz_pool: Nat, token_pool: Nat, nat_amount: Nat) -> Nat:
    if curve_id == AMM:
        return amm_tokens_bought(xtz_pool, token_pool, nat_amount)
    else:
        return cfmm_tokens_bought(FlatSwapParam(xtz_pool, token_pool, nat_amount))

def get_xtz_bought(curve_id: Nat, token_pool: Nat, xtz_pool: Nat, nat_amount: Nat) -> Nat:
    if curve_id == AMM:
        return amm_tokens_bought(token_pool, xtz_pool, nat_amount)
    else:
        return cfmm_tokens_bought(FlatSwapParam(token_pool, xtz_pool, nat_amount))


@dataclass(kw_only=True)
class Dexter(BaseContract):
    token_pool: Nat
    xtz_pool: Mutez
    lqt_total: Nat
    token_address: Address
    lqt_address: Address
    curve_id: Nat

    def add_liquidity(
            self,
            owner: Address,
            min_lqt_minted: Nat,
            max_tokens_deposited: Nat,
            deadline: Timestamp,
    ) -> None:
        if Timestamp.now() >= deadline:
            raise Exception("The current time must be less than the deadline.")
        else:
            xtz_pool = mutez_to_natural(self.xtz_pool)
            nat_amount = mutez_to_natural(Tezos.amount)
            lqt_minted = nat_amount * self.lqt_total // xtz_pool
            tokens_deposited = ceildiv(nat_amount * self.token_pool, xtz_pool)

            if tokens_deposited > max_tokens_deposited:
                raise Exception("Max tokens deposited must be greater than or equal to tokens deposited")
            elif lqt_minted < min_lqt_minted:
                raise Exception("Lqt minted must be greater than min lqt minted")
            else:
                self.lqt_total = self.lqt_total + lqt_minted
                self.token_pool = self.token_pool + tokens_deposited
                self.xtz_pool = self.xtz_pool + Tezos.amount

                self.ops = token_transfer(self.ops, self.token_address, Tezos.sender, Tezos.self_address, tokens_deposited)
                self.ops = mint_or_burn(self.ops, self.lqt_address, owner, lqt_minted.to_int())

    def remove_liquidity(
        self,
        to: Address,
        lqt_burned: Nat,
        min_xtz_withdrawn: Mutez,
        min_tokens_withdrawn: Nat,
        deadline: Timestamp,
    ) -> None:
        if Timestamp.now() >= deadline:
            raise Exception("The current time must be less than the deadline")
        elif Tezos.amount > Mutez(0):
            raise Exception("Amount must be zero")
        else:
            xtz_withdrawn = natural_to_mutez(lqt_burned * mutez_to_natural(self.xtz_pool) // self.lqt_total)
            tokens_withdrawn = lqt_burned * self.token_pool // self.lqt_total

            if xtz_withdrawn < min_xtz_withdrawn:
                raise Exception("The amount of xtz withdrawn must be greater than or equal to min xtz withdrawn")
            elif tokens_withdrawn < min_tokens_withdrawn:
                raise Exception("The amount of tokens withdrawn must be greater than or equal to min tokens withdrawn")
            else:
                self.lqt_total = (self.lqt_total - lqt_burned).is_nat().get("Cannot burn more than the total amount of lqt")
                self.token_pool = (self.token_pool - tokens_withdrawn).is_nat().get( "Token pool minus tokens withdrawn is negative")

                self.ops = mint_or_burn(self.ops, self.lqt_address, Tezos.sender, (Nat(0) - lqt_burned))
                self.ops = token_transfer(self.ops, self.token_address, Tezos.self_address, to, tokens_withdrawn)
                self.ops = xtz_transfer(self.ops, to, xtz_withdrawn)

    def xtz_to_token(self, to: Address, min_tokens_bought: Nat, deadline: Timestamp) -> None:
        if Timestamp.now() >= deadline:
            raise Exception("The current time must be less than the deadline")
        else:
            # we don't check that xtz_pool > 0, because that is impossible
            # unless all liquidity has been removed
            xtz_pool = mutez_to_natural(self.xtz_pool)
            nat_amount = mutez_to_natural(Tezos.amount)
            tokens_bought = get_tokens_bought(self.curve_id, xtz_pool, self.token_pool, nat_amount)
            if tokens_bought < min_tokens_bought:
                raise Exception("Tokens bought must be greater than or equal to min tokens bought")
            self.token_pool = (self.token_pool - tokens_bought).is_nat().get("Token pool minus tokens bought is negative")
            self.xtz_pool = self.xtz_pool + Tezos.amount
            self.ops = token_transfer(self.ops, self.token_address, Tezos.self_address, to, tokens_bought)

    def token_to_xtz(self, to: Address, tokens_sold: Nat, min_xtz_bought: Mutez, deadline: Timestamp) -> None:
        if Timestamp.now() >= deadline:
            raise Exception("The current time must be less than the deadline")
        elif Tezos.amount > Mutez(0):
            raise Exception("Amount must be zero")
        else:
            # we don't check that tokenPool > 0, because that is impossible
            # unless all liquidity has been removed
            xtz_bought = natural_to_mutez(get_xtz_bought(self.curve_id, self.token_pool, mutez_to_natural(self.xtz_pool), tokens_sold))
            if xtz_bought < min_xtz_bought:
                raise Exception("Xtz bought must be greater than or equal to min xtz bought")
            self.ops = token_transfer(self.ops, self.token_address, Tezos.sender, Tezos.self_address, tokens_sold)
            self.ops = xtz_transfer(self.ops, to, xtz_bought)
            self.token_pool = self.token_pool + tokens_sold
            self.xtz_pool = (self.xtz_pool - xtz_bought).get("negative mutez")

Election#

from dataclasses import dataclass
from pymich.michelson_types import *


def require(condition: Boolean, message: String) -> Nat:
    if not condition:
        raise Exception(message)

    return Nat(0)


@dataclass(kw_only=True)
class Election(BaseContract):
    admin: Address
    manifest_url: String
    manifest_hash: String
    _open: String
    _close: String
    artifacts_url: String
    artifacts_hash: String

    def open(self, _open: String, manifest_url: String, manifest_hash: String) -> None:
        require(Tezos.sender == self.admin, String("Only admin can call this entrypoint"))
        self._open = _open
        self.manifest_url = manifest_url
        self.manifest_hash = manifest_hash

    def close(self, _close: String) -> None:
        require(Tezos.sender == self.admin, String("Only admin can call this entrypoint"))
        self._close = _close

    def artifacts(self, artifacts_url: String, artifacts_hash: String) -> None:
        require(Tezos.sender == self.admin, String("Only admin can call this entrypoint"))
        self.artifacts_url = artifacts_url
        self.artifacts_hash = artifacts_hash

Escrow#

from dataclasses import dataclass
from pymich.michelson_types import *


@dataclass(kw_only=True)
class Escrow(BaseContract):
    seller: Address
    buyer: Address
    price: Mutez
    paid: Boolean
    confirmed: Boolean

    def pay(self) -> None:
        if Tezos.sender != self.buyer:
            raise Exception("You are not the seller")

        if Tezos.amount != self.price:
            raise Exception("Not the right price")

        if self.paid:
            raise Exception("You have already paid!")

        self.paid = True

    def confirm(self) -> None:
        if Tezos.sender != self.buyer:
            raise Exception("You are not the buyer")

        if not self.paid:
            raise Exception("You have not paid")

        if self.confirmed:
            raise Exception("Already confirmed")

        self.confirmed = True

    def claim(self) -> None:
        if Tezos.sender != self.seller:
            raise Exception("You are not the seller")

        if not self.confirmed:
            raise Exception("Not confirmed")

        self.ops = self.ops.push(Contract[Unit](Tezos.sender), Tezos.balance, Unit())

Lottery#

from dataclasses import dataclass

from pymich.michelson_types import *
from pymich.stdlib import *


@dataclass
class BidInfo(Record):
    value_hash: Bytes
    num_bid: Nat


@dataclass(kw_only=True)
class Lottery(BaseContract):
    bid_amount: Mutez
    deadline_bet: Timestamp
    deadline_reveal: Timestamp
    bids: BigMap[Address, BidInfo]
    nb_bids: Nat
    nb_revealed: Nat
    sum_values: Nat

    def bet(self, value_hash: Bytes) -> None:
        if Tezos.sender in self.bids:
            raise Exception("You have already made a bid")

        if Tezos.amount != self.bid_amount:
            raise Exception("You have not bidded the right amount")

        if Timestamp.now() > self.deadline_bet:
            raise Exception("Too late to make a bid")

        self.bids[Tezos.sender] = BidInfo(value_hash, self.nb_bids)
        self.nb_bids = self.nb_bids + Nat(1)

    def reveal(self, value: Nat) -> None:
        if not (Tezos.sender in self.bids):
            raise Exception("You have not made a bid")

        if Timestamp.now() > self.deadline_bet or Timestamp.now() > self.deadline_reveal:
            raise Exception("Too late to make a reveal")

        if Bytes(value).blake2b() != self.bids[Tezos.sender].value_hash:
            raise Exception("Wrong hash")

        self.sum_values = self.sum_values + value
        self.nb_revealed = self.nb_revealed + Nat(1)

    def claim(self) -> None:
        if Timestamp.now() < self.deadline_reveal:
            raise Exception("The lottery is not over")

        if not (Tezos.sender in self.bids):
            raise Exception("You have not made a bid")

        num_winner = self.sum_values % self.nb_revealed

        if self.bids[Tezos.sender].num_bid != num_winner:
            raise Exception("You have not won")

        transaction(Contract[Unit](Tezos.sender), Tezos.balance, Unit())

Notarization#

from dataclasses import dataclass
from pymich.michelson_types import *


@dataclass(eq=False)
class DocumentId(Record):
    owner: Address
    uuid: String


@dataclass(kw_only=True)
class Notarization(BaseContract):
    admin: Address
    document_hashes: BigMap[DocumentId, Bytes]

    def add_document(self, document_uuid: String, document_hash: Bytes) -> None:
        self.document_hashes[DocumentId(Tezos.sender, document_uuid)] = document_hash

    def get_hash(self, document_uuid: String, owner: Address) -> Bytes:
        return self.document_hashes[DocumentId(owner, document_uuid)]

Upgradable contract#

from dataclasses import dataclass

from pymich.michelson_types import *
from typing import Callable


@dataclass(kw_only=True)
class Upgradable(BaseContract):
    counter: Nat
    f: Callable[[Nat], Nat]

    def update_f(self, f: Callable[[Nat], Nat]) -> None:
        self.f = f

    def update_counter(self, x: Nat) -> None:
        self.counter = self.f(x)

Visitor#

from dataclasses import dataclass
from pymich.michelson_types import *


@dataclass
class VisitorInfo(Record):
    visits: Nat
    name: String
    last_visit: Timestamp


@dataclass(kw_only=True)
class Visitor(BaseContract):
    visitors: BigMap[Address, VisitorInfo]
    total_visits: Nat

    def register(self, name: String) -> None:
        self.visitors[Tezos.sender] = VisitorInfo(Nat(0), name, Timestamp.now())

    def visit(self) -> None:
        if not (Tezos.sender in self.visitors):
            raise Exception("You are not registered yet!")

        n_visits = self.visitors[Tezos.sender].visits

        if Timestamp.now() - self.visitors[Tezos.sender].last_visit < Int(10) * Int(24) * Int(3600):
            raise Exception("You need to wait 10 days between visits")

        if n_visits == Nat(0) and Tezos.amount != Mutez(5):
            raise Exception("You need to pay 5 mutez on your first visit!")

        if n_visits != Nat(0) and Tezos.amount != Mutez(3):
            raise Exception("You need to pay 3 mutez to visit!")

        self.visitors[Tezos.sender].visits = n_visits + Nat(1)
        self.total_visits = self.total_visits + Nat(1)