Tokens & DeFi

Panda supports fungible tokens (PRC-20), non-fungible tokens (PRC-721), and DeFi primitives like AMMs and marketplaces -- all written in Python.

PRC-20: Fungible Tokens

The PRC-20 standard provides mint, transfer, and approve functionality using the panda.token module. The reference implementation includes freeze/thaw authority, zero-value transfers, and proper event emission.

# Source: contracts/tokens/prc20_token.py
"""
PRC-20 reference contract — uses ``panda.token.FungibleToken``.

See ``docs/PRC20.md``. Deploy one contract per fungible token.
"""

from panda import contract, constructor, call, query, event

from panda.token import FungibleToken, TokenError


@contract
class PRC20Token:
    class State:
        token: dict = {}
        mint_authority: str = ""
        freeze_authority: str = ""
        frozen: dict = {}

    def _t(self) -> FungibleToken:
        return FungibleToken(self.state.token)

    def _not_frozen(self, addr: str):
        if self.state.frozen.get(addr, False):
            raise TokenError(f"frozen: {addr}")

    def _init_token(
        self,
        ctx,
        name: str,
        symbol: str,
        decimals: int = 18,
        max_supply: int = 0,
        initial_supply: int = 0,
    ):
        t = self._t()
        if t.name() or t.total_supply() > 0:
            raise TokenError("already initialized")
        t.configure(name=name, symbol=symbol, decimals=decimals, max_supply=max_supply)
        self.state.mint_authority = ctx.sender
        self.state.freeze_authority = ctx.sender
        if initial_supply > 0:
            t.mint(ctx.sender, initial_supply)
        self.state.token = t.to_dict()
        self.emit(
            event.PRC20Initialized(
                deployer=ctx.sender,
                name=name,
                symbol=symbol,
                decimals=decimals,
                initial_supply=initial_supply,
            )
        )
        print(
            f"PRC20 deployed: {name} ({symbol}) decimals={decimals} initial_supply={initial_supply} by {ctx.sender}"
        )

    @constructor
    def deploy(
        self,
        ctx,
        name: str,
        symbol: str,
        decimals: int = 18,
        max_supply: int = 0,
        initial_supply: int = 0,
    ):
        """Runs at deploy; pass matching JSON to ``panda deploy --args``."""
        self._init_token(ctx, name, symbol, decimals, max_supply, initial_supply)

    @call
    def transfer(self, ctx, to: str, value: int):
        self._not_frozen(ctx.sender)
        self._not_frozen(to)
        t = self._t()
        if value == 0:
            self.state.token = t.to_dict()
            self.emit(event.Transfer(_from=ctx.sender, _to=to, _value=0))
            print(f"Transfer: {ctx.sender} -> {to} amount=0")
            return
        t.transfer(ctx.sender, to, value)
        self.state.token = t.to_dict()
        self.emit(event.Transfer(_from=ctx.sender, _to=to, _value=value))
        print(f"Transfer: {ctx.sender} -> {to} amount={value}")

    @call
    def approve(self, ctx, spender: str, value: int):
        self._not_frozen(ctx.sender)
        t = self._t()
        t.approve(ctx.sender, spender, value)
        self.state.token = t.to_dict()
        self.emit(event.Approval(_owner=ctx.sender, _spender=spender, _value=value))
        print(f"Approval: {ctx.sender} approved {spender} for {value}")

    @call
    def transfer_from(self, ctx, owner: str, to: str, value: int):
        self._not_frozen(owner)
        self._not_frozen(to)
        t = self._t()
        if value == 0:
            self.state.token = t.to_dict()
            self.emit(event.Transfer(_from=owner, _to=to, _value=0))
            return
        t.transfer_from(ctx.sender, owner, to, value)
        self.state.token = t.to_dict()
        self.emit(event.Transfer(_from=owner, _to=to, _value=value))
        print(f"TransferFrom: {owner} -> {to} amount={value} (spender={ctx.sender})")

    @call
    def mint(self, ctx, to: str, amount: int):
        if ctx.sender != self.state.mint_authority:
            raise TokenError("not mint authority")
        t = self._t()
        t.mint(to, amount)
        self.state.token = t.to_dict()
        self.emit(
            event.Transfer(
                _from="0x0000000000000000000000000000000000000000",
                _to=to,
                _value=amount,
            )
        )
        print(f"Minted {amount} to {to} by authority {ctx.sender}")

    @query
    def balance_of(self, owner: str) -> int:
        return self._t().balance_of(owner)

    @query
    def total_supply(self) -> int:
        return self._t().total_supply()

    @call
    def freeze_account(self, ctx, target: str):
        if ctx.sender != self.state.freeze_authority:
            raise TokenError("not freeze authority")
        f = dict(self.state.frozen)
        f[target] = True
        self.state.frozen = f
        print(f"Account frozen: {target} by authority {ctx.sender}")

    @call
    def thaw_account(self, ctx, target: str):
        if ctx.sender != self.state.freeze_authority:
            raise TokenError("not freeze authority")
        f = dict(self.state.frozen)
        f[target] = False
        self.state.frozen = f
        print(f"Account thawed: {target} by authority {ctx.sender}")

Key patterns in the real contract:

  • Helper methods like _t() and _not_frozen() keep the code DRY -- _t() wraps FungibleToken deserialization, _not_frozen() checks the freeze list
  • Freeze/thaw authority is separate from mint authority, allowing different admin roles
  • Zero-value transfers are handled explicitly (skip the FungibleToken logic but still emit the event)
  • _init_token() guards against double-init by checking if name() or total_supply() is already set

FungibleToken API

MethodDescription
configure(name, symbol, decimals, max_supply)Initialize token metadata
mint(to, amount)Create new tokens
transfer(from_addr, to_addr, amount)Move tokens between addresses
approve(owner, spender, amount)Grant spend allowance
transfer_from(spender, from_addr, to_addr, amount)Spend approved tokens
balance_of(owner)Check balance
total_supply()Get total minted supply
to_dict() / constructorSerialize/deserialize state

See the full PRC-20 Standard for the complete specification.

PRC-721: Non-Fungible Tokens

NFTs represent unique digital assets. Each token has a unique ID and optional metadata URI. The reference implementation includes per-token approvals, operator approvals (approve-for-all), and safe transfers.

# Source: contracts/tokens/prc721_collection.py
"""
PRC-721 reference collection — ownership, approvals, and transfers.

See ``docs/PRC721.md``. One deployed contract = one NFT collection.
"""

from panda import contract, constructor, call, query, event

ZERO = "0x0000000000000000000000000000000000000000"


def _is_authorized_transfer(
    owner: str, sender: str, token_id: int, approvals: dict, op: dict
) -> bool:
    if sender == owner:
        return True
    if approvals.get(str(token_id)) == sender:
        return True
    ops = op.get(owner, {})
    return bool(ops.get(sender, False))


def _can_approve(owner: str, sender: str, op: dict) -> bool:
    if sender == owner:
        return True
    inner = op.get(owner, {})
    return bool(inner.get(sender, False))


@contract
class PRC721Collection:
    class State:
        name: str = ""
        symbol: str = ""
        base_uri: str = ""
        tokens: dict = {}
        owner_balances: dict = {}
        token_approvals: dict = {}
        operator_approvals: dict = {}

    def _token_owner(self, token_id: int) -> str:
        key = str(token_id)
        if key not in self.state.tokens:
            raise ValueError("token does not exist")
        return self.state.tokens[key]["owner"]

    def _set_owner_balance(self, owner: str, delta: int):
        b = dict(self.state.owner_balances)
        cur = b.get(owner, 0)
        b[owner] = cur + delta
        self.state.owner_balances = b

    @constructor
    def deploy(self, ctx, name: str, symbol: str, base_uri: str = ""):
        self.state.name = name
        self.state.symbol = symbol
        self.state.base_uri = base_uri
        print(f"PRC721 collection deployed: {name} ({symbol}) by {ctx.sender}")

    @call
    def mint(self, ctx, to: str, token_uri: str = ""):
        tid = len(self.state.tokens) + 1
        key = str(tid)
        tok = dict(self.state.tokens)
        tok[key] = {"owner": to, "uri": token_uri}
        self.state.tokens = tok
        self._set_owner_balance(to, 1)
        self.emit(event.Transfer(_from=ZERO, _to=to, _token_id=tid))
        print(f"Minted NFT #{tid} to {to}")

    @call
    def approve(self, ctx, to: str, token_id: int):
        owner = self._token_owner(token_id)
        if not _can_approve(owner, ctx.sender, dict(self.state.operator_approvals)):
            raise ValueError("not authorized")
        ta = dict(self.state.token_approvals)
        ta[str(token_id)] = to
        self.state.token_approvals = ta
        self.emit(event.Approval(_owner=owner, _approved=to, _token_id=token_id))
        print(f"Approved {to} for NFT #{token_id} (owner={owner})")

    @call
    def transfer_from(self, ctx, from_addr: str, to_addr: str, token_id: int):
        owner = self._token_owner(token_id)
        if owner != from_addr:
            raise ValueError("from mismatch")
        if not _is_authorized_transfer(
            owner,
            ctx.sender,
            token_id,
            dict(self.state.token_approvals),
            dict(self.state.operator_approvals),
        ):
            raise ValueError("not authorized")
        key = str(token_id)
        tok = dict(self.state.tokens)
        tok[key] = {**tok[key], "owner": to_addr}
        self.state.tokens = tok
        self._set_owner_balance(from_addr, -1)
        self._set_owner_balance(to_addr, 1)
        ta = dict(self.state.token_approvals)
        if str(token_id) in ta:
            del ta[str(token_id)]
        self.state.token_approvals = ta
        self.emit(event.Transfer(_from=from_addr, _to=to_addr, _token_id=token_id))
        print(f"NFT #{token_id} transferred: {from_addr} -> {to_addr}")

    @query
    def balance_of(self, owner: str) -> int:
        if owner == ZERO:
            raise ValueError("invalid owner")
        return self.state.owner_balances.get(owner, 0)

    @query
    def owner_of(self, token_id: int) -> str:
        return self._token_owner(token_id)

    @query
    def token_uri(self, token_id: int) -> str:
        key = str(token_id)
        if key not in self.state.tokens:
            raise ValueError("token does not exist")
        meta = self.state.tokens[key].get("uri", "")
        base = self.state.base_uri
        if base and not meta:
            return base.rstrip("/") + "/" + key
        return meta

Key patterns:

  • Authorization is multi-layered: owner, per-token approval, and operator approval (approve-for-all) are all checked via _is_authorized_transfer()
  • Token IDs use str() keys because JSON only supports string keys
  • Always create a new dict with dict(self.state.X) before mutating
  • transfer_from clears per-token approval after transfer (prevents stale approvals)
  • base_uri provides automatic token URI generation when individual URIs are not set

See the full PRC-721 Standard for the complete specification.

NFT Marketplace

The NFT Marketplace demonstrates cross-contract composition: it calls PRC-721 contracts to verify ownership and transfer NFTs, and PRC-20 contracts to handle payments -- all atomically within a single transaction.

# Source: contracts/tokens/nft_marketplace.py
"""
NFT Marketplace — Cross-contract call example: buy/sell NFTs using PRC-20 tokens.

Demonstrates cross-contract interaction between PRC-721 (NFT) and PRC-20 (payment)
contracts using the Pythonic ``Contract`` and ``PRC20`` handles.
"""

from panda import contract, constructor, call, query, event, Contract, PRC20


@contract
class NFTMarketplace:
    """Marketplace for buying/selling PRC-721 NFTs with PRC-20 payment."""

    class State:
        payment_token: str = ""
        listings: dict = {}
        fee_bps: int = 250  # 2.5% marketplace fee
        fee_recipient: str = ""
        total_volume: int = 0
        total_sales: int = 0

    @constructor
    def deploy(self, ctx, payment_token: str, fee_bps: int = 250):
        self.state.payment_token = payment_token
        self.state.fee_bps = fee_bps
        self.state.fee_recipient = ctx.sender
        print(
            f"NFTMarketplace deployed: payment_token={payment_token} fee={fee_bps}bps by {ctx.sender}"
        )

    @call
    def list_nft(self, ctx, nft_contract: str, token_id: int, price: int):
        """List an NFT for sale. Seller must have approved the marketplace."""
        if price <= 0:
            raise ValueError("price must be positive")

        # Verify the caller owns the NFT
        owner = Contract(nft_contract).owner_of(token_id=token_id)
        if owner != ctx.sender:
            raise ValueError("caller does not own this NFT")

        listing_id = f"{nft_contract}:{token_id}"
        listings = dict(self.state.listings)
        listings[listing_id] = {
            "seller": ctx.sender,
            "nft_contract": nft_contract,
            "token_id": token_id,
            "price": price,
            "active": True,
        }
        self.state.listings = listings

        self.emit(
            event.NFTListed(
                seller=ctx.sender,
                nft_contract=nft_contract,
                token_id=token_id,
                price=price,
            )
        )
        print(f"NFT listed: {nft_contract}#{token_id} by {ctx.sender} for {price}")

    @call
    def buy_nft(self, ctx, nft_contract: str, token_id: int):
        """Buy a listed NFT. Buyer must have approved payment token spending."""
        listing_id = f"{nft_contract}:{token_id}"
        listings = dict(self.state.listings)
        listing = listings.get(listing_id)
        if not listing or not listing["active"]:
            raise ValueError("listing not found or inactive")

        price = listing["price"]
        seller = listing["seller"]

        # Calculate fees
        fee = (price * self.state.fee_bps) // 10000
        seller_proceeds = price - fee

        # Transfer payment from buyer to seller
        payment = PRC20(self.state.payment_token)
        payment.transfer_from(owner=ctx.sender, to=seller, value=seller_proceeds)

        # Transfer fee to fee recipient
        if fee > 0:
            payment.transfer_from(owner=ctx.sender, to=self.state.fee_recipient, value=fee)

        # Transfer NFT from seller to buyer
        Contract(nft_contract).transfer_from(
            from_addr=seller, to_addr=ctx.sender, token_id=token_id
        )

        # Mark listing as inactive
        listing["active"] = False
        listings[listing_id] = listing
        self.state.listings = listings
        self.state.total_volume = self.state.total_volume + price
        self.state.total_sales = self.state.total_sales + 1

        self.emit(
            event.NFTSold(
                buyer=ctx.sender,
                seller=seller,
                nft_contract=nft_contract,
                token_id=token_id,
                price=price,
                fee=fee,
            )
        )

    @call
    def cancel_listing(self, ctx, nft_contract: str, token_id: int):
        """Cancel an active listing. Only the seller can cancel."""
        listing_id = f"{nft_contract}:{token_id}"
        listings = dict(self.state.listings)
        listing = listings.get(listing_id)
        if not listing or not listing["active"]:
            raise ValueError("listing not found or inactive")
        if listing["seller"] != ctx.sender:
            raise ValueError("only seller can cancel")

        listing["active"] = False
        listings[listing_id] = listing
        self.state.listings = listings

        self.emit(
            event.ListingCancelled(
                seller=ctx.sender,
                nft_contract=nft_contract,
                token_id=token_id,
            )
        )

    @query
    def get_listing(self, nft_contract: str, token_id: int) -> dict:
        listing_id = f"{nft_contract}:{token_id}"
        listing = self.state.listings.get(listing_id)
        if not listing:
            return {}
        return listing

    @query
    def get_stats(self) -> dict:
        return {
            "total_volume": self.state.total_volume,
            "total_sales": self.state.total_sales,
            "fee_bps": self.state.fee_bps,
        }

Cross-contract handles are the key pattern here:

  • Contract(address) gives a generic handle -- call any method on any deployed contract
  • PRC20(address) gives a typed handle with .transfer(), .transfer_from(), .balance_of() etc.
  • buy_nft performs three cross-contract calls atomically: payment to seller, fee to recipient, and NFT transfer to buyer

Token Swap (AMM)

The TokenSwap contract implements a constant-product automated market maker (AMM) for swapping between two PRC-20 tokens. It uses the formula x * y = k to price trades.

# Source: contracts/tokens/token_swap.py
"""
Token Swap — Cross-contract call example: simple AMM-like token swap.

Each pool holds reserves of two PRC-20 tokens (token_a, token_b).
Swaps use the constant-product formula: x * y = k.
"""

from panda import contract, constructor, call, query, event, PRC20


@contract
class TokenSwap:
    """Simple AMM swap pool for two PRC-20 tokens via cross-contract calls."""

    class State:
        token_a: str = ""
        token_b: str = ""
        reserve_a: int = 0
        reserve_b: int = 0
        total_lp: int = 0
        lp_balances: dict = {}
        fee_bps: int = 30  # 0.3% fee in basis points
        owner: str = ""

    @constructor
    def deploy(self, ctx, token_a: str, token_b: str, fee_bps: int = 30):
        if token_a == token_b:
            raise ValueError("tokens must be different")
        self.state.token_a = token_a
        self.state.token_b = token_b
        self.state.fee_bps = fee_bps
        self.state.owner = ctx.sender
        print(
            f"TokenSwap pool deployed: token_a={token_a} token_b={token_b} fee={fee_bps}bps by {ctx.sender}"
        )

    @call
    def add_liquidity(self, ctx, amount_a: int, amount_b: int):
        """Add liquidity to the pool. Transfers tokens from sender via cross-call."""
        if amount_a <= 0 or amount_b <= 0:
            raise ValueError("amounts must be positive")

        # Transfer tokens into the pool
        PRC20(self.state.token_a).transfer_from(
            owner=ctx.sender, to=ctx.contract_address, value=amount_a
        )
        PRC20(self.state.token_b).transfer_from(
            owner=ctx.sender, to=ctx.contract_address, value=amount_b
        )

        # Calculate LP tokens to mint
        if self.state.total_lp == 0:
            # First deposit: LP = sqrt(a * b) approximated as (a + b) / 2
            lp_amount = (amount_a + amount_b) // 2
        else:
            lp_a = amount_a * self.state.total_lp // self.state.reserve_a
            lp_b = amount_b * self.state.total_lp // self.state.reserve_b
            lp_amount = min(lp_a, lp_b)

        if lp_amount <= 0:
            raise ValueError("insufficient liquidity minted")

        self.state.reserve_a = self.state.reserve_a + amount_a
        self.state.reserve_b = self.state.reserve_b + amount_b
        self.state.total_lp = self.state.total_lp + lp_amount
        lp = dict(self.state.lp_balances)
        lp[ctx.sender] = lp.get(ctx.sender, 0) + lp_amount
        self.state.lp_balances = lp

        self.emit(
            event.LiquidityAdded(
                provider=ctx.sender,
                amount_a=amount_a,
                amount_b=amount_b,
                lp_minted=lp_amount,
            )
        )

    @call
    def swap_a_for_b(self, ctx, amount_in: int):
        """Swap token_a for token_b using constant-product formula."""
        if amount_in <= 0:
            raise ValueError("amount must be positive")
        if self.state.reserve_a == 0 or self.state.reserve_b == 0:
            raise ValueError("no liquidity")

        # Apply fee
        amount_in_with_fee = amount_in * (10000 - self.state.fee_bps)
        amount_out = (amount_in_with_fee * self.state.reserve_b) // (
            self.state.reserve_a * 10000 + amount_in_with_fee
        )

        if amount_out <= 0:
            raise ValueError("insufficient output amount")

        # Transfer token_a from sender into pool
        PRC20(self.state.token_a).transfer_from(
            owner=ctx.sender, to=ctx.contract_address, value=amount_in
        )
        # Transfer token_b from pool to sender
        PRC20(self.state.token_b).transfer(to=ctx.sender, value=amount_out)

        self.state.reserve_a = self.state.reserve_a + amount_in
        self.state.reserve_b = self.state.reserve_b - amount_out

        self.emit(
            event.Swap(
                sender=ctx.sender,
                token_in=self.state.token_a,
                amount_in=amount_in,
                amount_out=amount_out,
            )
        )

    @call
    def swap_b_for_a(self, ctx, amount_in: int):
        """Swap token_b for token_a using constant-product formula."""
        if amount_in <= 0:
            raise ValueError("amount must be positive")
        if self.state.reserve_a == 0 or self.state.reserve_b == 0:
            raise ValueError("no liquidity")

        amount_in_with_fee = amount_in * (10000 - self.state.fee_bps)
        amount_out = (amount_in_with_fee * self.state.reserve_a) // (
            self.state.reserve_b * 10000 + amount_in_with_fee
        )

        if amount_out <= 0:
            raise ValueError("insufficient output amount")

        PRC20(self.state.token_b).transfer_from(
            owner=ctx.sender, to=ctx.contract_address, value=amount_in
        )
        PRC20(self.state.token_a).transfer(to=ctx.sender, value=amount_out)

        self.state.reserve_b = self.state.reserve_b + amount_in
        self.state.reserve_a = self.state.reserve_a - amount_out

        self.emit(
            event.Swap(
                sender=ctx.sender,
                token_in=self.state.token_b,
                amount_in=amount_in,
                amount_out=amount_out,
            )
        )

    @query
    def get_reserves(self) -> dict:
        return {
            "reserve_a": self.state.reserve_a,
            "reserve_b": self.state.reserve_b,
            "total_lp": self.state.total_lp,
        }

    @query
    def get_lp_balance(self, owner: str) -> int:
        return self.state.lp_balances.get(owner, 0)

    @query
    def quote_swap_a_for_b(self, amount_in: int) -> int:
        """Preview how much token_b you'd get for amount_in of token_a."""
        if self.state.reserve_a == 0 or self.state.reserve_b == 0:
            return 0
        amount_in_with_fee = amount_in * (10000 - self.state.fee_bps)
        return (amount_in_with_fee * self.state.reserve_b) // (
            self.state.reserve_a * 10000 + amount_in_with_fee
        )

How the AMM works:

  • The constant-product formula x * y = k ensures that larger trades get progressively worse prices (slippage), which protects liquidity providers
  • Fees are applied before the swap calculation using basis points (30 bps = 0.3%)
  • LP tokens track each provider's share of the pool -- first deposit uses (a + b) / 2, subsequent deposits use proportional minting via min(lp_a, lp_b)
  • quote_swap_a_for_b lets users preview their output amount without executing

Token Interfaces (IPRC-20 / IPRC-721)

Panda defines abstract interfaces for token standards, analogous to Solidity's interface keyword. Contracts can inherit from these to guarantee compliance. All methods raise NotImplementedError -- subclasses must override them.

IPRC20 Interface

<!-- Source: contracts/tokens/iprc20.py -->
from panda import contract, call, query


@contract
class IPRC20:
    """Abstract PRC-20 interface. Inherit and override all methods."""

    class State:
        _interface_marker: str = "IPRC20"

    # ---- Metadata (queries) ----

    @query
    def name(self) -> str:
        raise NotImplementedError("IPRC20.name")

    @query
    def symbol(self) -> str:
        raise NotImplementedError("IPRC20.symbol")

    @query
    def decimals(self) -> int:
        raise NotImplementedError("IPRC20.decimals")

    @query
    def total_supply(self) -> int:
        raise NotImplementedError("IPRC20.total_supply")

    @query
    def balance_of(self, owner: str) -> int:
        raise NotImplementedError("IPRC20.balance_of")

    @query
    def allowance(self, owner: str, spender: str) -> int:
        raise NotImplementedError("IPRC20.allowance")

    # ---- Mutations (calls) ----

    @call
    def transfer(self, ctx, to: str, value: int):
        raise NotImplementedError("IPRC20.transfer")

    @call
    def approve(self, ctx, spender: str, value: int):
        raise NotImplementedError("IPRC20.approve")

    @call
    def transfer_from(self, ctx, owner: str, to: str, value: int):
        raise NotImplementedError("IPRC20.transfer_from")

IPRC721 Interface

<!-- Source: contracts/tokens/iprc721.py -->
from panda import contract, call, query


@contract
class IPRC721:
    """Abstract PRC-721 interface. Inherit and override all methods."""

    class State:
        _interface_marker: str = "IPRC721"

    @query
    def name(self) -> str:
        raise NotImplementedError("IPRC721.name")

    @query
    def symbol(self) -> str:
        raise NotImplementedError("IPRC721.symbol")

    @query
    def balance_of(self, owner: str) -> int:
        raise NotImplementedError("IPRC721.balance_of")

    @query
    def owner_of(self, token_id: int) -> str:
        raise NotImplementedError("IPRC721.owner_of")

    @query
    def get_approved(self, token_id: int) -> str:
        raise NotImplementedError("IPRC721.get_approved")

    @query
    def is_approved_for_all(self, owner: str, operator: str) -> bool:
        raise NotImplementedError("IPRC721.is_approved_for_all")

    @query
    def token_uri(self, token_id: int) -> str:
        raise NotImplementedError("IPRC721.token_uri")

    @call
    def mint(self, ctx, to: str, token_uri: str = ""):
        raise NotImplementedError("IPRC721.mint")

    @call
    def approve(self, ctx, to: str, token_id: int):
        raise NotImplementedError("IPRC721.approve")

    @call
    def set_approval_for_all(self, ctx, operator: str, approved: bool):
        raise NotImplementedError("IPRC721.set_approval_for_all")

    @call
    def transfer_from(self, ctx, from_addr: str, to_addr: str, token_id: int):
        raise NotImplementedError("IPRC721.transfer_from")

    @call
    def safe_transfer_from(self, ctx, from_addr: str, to_addr: str, token_id: int):
        raise NotImplementedError("IPRC721.safe_transfer_from")

Implementing an Interface

The PRC20WithInterface contract shows how to implement the IPRC20 interface. It uses panda.token.FungibleToken for core logic and overrides every interface method:

<!-- Source: contracts/tokens/prc20_interface.py -->
from panda import contract, constructor, call, query, event
from panda.token import FungibleToken, TokenError


@contract
class PRC20WithInterface:
    """PRC-20 token that explicitly implements the IPRC20 interface."""

    class State:
        token: dict = {}
        owner: str = ""

    @constructor
    def deploy(self, ctx, name: str, symbol: str, decimals: int = 18, initial_supply: int = 0):
        t = FungibleToken()
        t.configure(name=name, symbol=symbol, decimals=decimals)
        if initial_supply > 0:
            t.mint(ctx.sender, initial_supply)
        self.state.token = t.to_dict()
        self.state.owner = ctx.sender
        self.emit(
            event.Transfer(
                _from="0x0000000000000000000000000000000000000000",
                _to=ctx.sender,
                _value=initial_supply,
            )
        )

    # ---- IPRC20 interface implementation ----

    @query
    def name(self) -> str:
        return FungibleToken(self.state.token).name()

    @query
    def symbol(self) -> str:
        return FungibleToken(self.state.token).symbol()

    @query
    def decimals(self) -> int:
        return FungibleToken(self.state.token).decimals()

    @query
    def total_supply(self) -> int:
        return FungibleToken(self.state.token).total_supply()

    @query
    def balance_of(self, owner: str) -> int:
        return FungibleToken(self.state.token).balance_of(owner)

    @query
    def allowance(self, owner: str, spender: str) -> int:
        return FungibleToken(self.state.token).allowance(owner, spender)

    @call
    def transfer(self, ctx, to: str, value: int):
        t = FungibleToken(self.state.token)
        t.transfer(ctx.sender, to, value)
        self.state.token = t.to_dict()
        self.emit(event.Transfer(_from=ctx.sender, _to=to, _value=value))

    @call
    def approve(self, ctx, spender: str, value: int):
        t = FungibleToken(self.state.token)
        t.approve(ctx.sender, spender, value)
        self.state.token = t.to_dict()
        self.emit(event.Approval(_owner=ctx.sender, _spender=spender, _value=value))

    @call
    def transfer_from(self, ctx, owner: str, to: str, value: int):
        t = FungibleToken(self.state.token)
        t.transfer_from(ctx.sender, owner, to, value)
        self.state.token = t.to_dict()
        self.emit(event.Transfer(_from=owner, _to=to, _value=value))

    # ---- Additional methods beyond IPRC20 ----

    @call
    def mint(self, ctx, to: str, amount: int):
        if ctx.sender != self.state.owner:
            raise TokenError("only owner can mint")
        t = FungibleToken(self.state.token)
        t.mint(to, amount)
        self.state.token = t.to_dict()
        self.emit(
            event.Transfer(
                _from="0x0000000000000000000000000000000000000000",
                _to=to,
                _value=amount,
            )
        )

    @call
    def burn(self, ctx, amount: int):
        t = FungibleToken(self.state.token)
        t.burn(ctx.sender, amount)
        self.state.token = t.to_dict()
        self.emit(
            event.Transfer(
                _from=ctx.sender,
                _to="0x0000000000000000000000000000000000000000",
                _value=amount,
            )
        )

The interface pattern ensures compliance: any contract that implements all IPRC20 methods is a valid PRC-20 token. The PRC721WithInterface contract follows the same pattern for NFTs, implementing all IPRC721 methods with ownership tracking, per-token approvals, operator approvals, and burn support.

Try It

  • Open the Playground and select the PRC-20 Token, PRC-721 NFT, NFT Marketplace, or Token Swap template
  • Follow the Minting NFTs tutorial for a guided walkthrough
  • Follow the DeFi: Token Swap tutorial to build an AMM