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)