Simple Staking Smart Contract POC

Video Walkthrough

Watch the tutorial video for an introduction to this staking smart contract:

Generating an Account

from algosdk.account import generate_account
  
private_key, address = generate_account()
print(private_key, address)

Funding the Account

To fund the account, visit the Algorand testnet dispenser: https://bank.testnet.algorand.network/

Algod Node API Information

Use the following Nodely API as your Algod Node API:

https://testnet-api.4160.nodely.dev

Documentation: https://nodely.io/docs/free/start

Staking Contract Code

from algopy import ARC4Contract, gtxn, Global, BoxRef, Txn, UInt64, itxn
from algopy.arc4 import abimethod
from algopy.arc4 import UInt64 as arc4UInt64

class StakingContract(ARC4Contract):
    def __init__(self) -> None:
        pass
    
    @abimethod
    def stake(self,
            pay_amount: gtxn.PaymentTransaction,
            fee_payment: gtxn.PaymentTransaction) -> tuple[UInt64, UInt64]:
        
        assert pay_amount.receiver == Global.current_application_address
        assert fee_payment.amount == 21_700
        
        staking_box = BoxRef(key=Txn.sender.bytes)
        value, exists = staking_box.maybe()
        
        if exists:
            amount_staked = arc4UInt64.from_bytes(value[0:8])
            initial_stake_time = arc4UInt64.from_bytes(value[8:16])
            time_difference = Global.latest_timestamp - initial_stake_time.native
            
            reward = time_difference * 1000
            itxn.Payment(receiver=Txn.sender,amount=reward,fee=Global.min_txn_fee).submit()
            
            new_total_stake = amount_staked.native + pay_amount.amount
            staking_box.put(arc4UInt64(new_total_stake).bytes + arc4UInt64(Global.latest_timestamp).bytes)
        
        else:        
            reward = UInt64(0)
            staking_box.create(size=16)
            staking_box.put(arc4UInt64(pay_amount.amount).bytes + arc4UInt64(Global.latest_timestamp).bytes)
        
        return pay_amount.amount, reward
    
    @abimethod
    def claimRewardAndWithdraw(self) -> UInt64:
        
        staking_box = BoxRef(key=Txn.sender.bytes)
        value, exists = staking_box.maybe()
        
        assert exists
        
        amount_staked = arc4UInt64.from_bytes(value[0:8])
        initial_stake_time = arc4UInt64.from_bytes(value[8:16])
        
        time_difference = Global.latest_timestamp - initial_stake_time.native
        reward = time_difference * 1000
        amount_to_send = reward + amount_staked.native
        
        itxn.Payment(receiver=Txn.sender,amount=amount_to_send,fee=Global.min_txn_fee).submit()
        staking_box.delete()
        
        return amount_to_send

Compiling the Contract

Use the following command to compile the contract:

algokit compile py MyStakingContract.py

Launching the Contract

from algosdk.transaction import ApplicationCreateTxn, StateSchema, OnComplete, wait_for_confirmation, PaymentTxn
from algosdk.account import address_from_private_key
from algosdk.v2client.algod import AlgodClient
from algosdk import logic
from dotenv import load_dotenv
import base64
import os

load_dotenv()

node_token = os.getenv('algod_token')
node_server = os.getenv('algod_server')
algod_client = AlgodClient(node_token, node_server)

private_key = os.getenv('private_key')
address = address_from_private_key(private_key)

params = algod_client.suggested_params()

approval_teal_file_name = 'StakingContract.approval.teal'
clear_teal_file_name = 'StakingContract.clear.teal'

with open(f'./{approval_teal_file_name}', 'r') as f:
    approval_teal_source = f.read()

with open(f'{clear_teal_file_name}', 'r') as f:
    clear_teal_source = f.read()

approval_result = algod_client.compile(approval_teal_source)
approval_program = base64.b64decode(approval_result['result'])

clear_result = algod_client.compile(clear_teal_source)
clear_program = base64.b64decode(clear_result['result'])

global_schema = StateSchema(num_uints=0, num_byte_slices=0)
local_schema = StateSchema(num_uints=0, num_byte_slices=0)

txn = ApplicationCreateTxn(
    sender = address,
    sp = params,
    on_complete=OnComplete.NoOpOC,
    approval_program=approval_program,
    clear_program=clear_program,
    global_schema=global_schema,
    local_schema=local_schema,
)
signed_txn = txn.sign(private_key)

try:
    txid = algod_client.send_transaction(signed_txn)
except Exception as e:
    print(e)
    
print(f'Tx ID: {txid}')
wait_for_confirmation(algod_client, txid)
tx_info = algod_client.pending_transaction_info(txid)
print(f'App ID: {tx_info["application-index"]}')

app_address = logic.get_application_address(tx_info["application-index"])
print(f'Application Address: {app_address}')

Funding the Contract

activate_contract = PaymentTxn (
    sender = address,
    sp = params,
    receiver = app_address,
    amt = 100_000
)
  
signed_activation = activate_contract.sign(private_key)
activation_tx = algod_client.send_transaction(signed_activation)
print(f'MBR For Contract to be Active Account Funded: {activation_tx}')

Box Fees and MBR Calculation

Box Minimum Balance Requirement (MBR) Calculation:

box_creation_fee = 2500
box_byte_fee = 400

address_bytes = 32
uint64_2 = 16

box_mbr = box_creation_fee + ((address_bytes + uint64_2) * 400)
print(box_mbr)

Read more about MBR for boxes: Algorand Box Storage Documentation

Staking to the Contract

from algokit_utils import ApplicationClient
from algosdk.v2client.algod import AlgodClient
from algosdk.atomic_transaction_composer import AccountTransactionSigner, AtomicTransactionComposer, TransactionWithSigner
from algosdk.transaction import PaymentTxn
from algosdk.account import address_from_private_key
from pathlib import Path
import os
from algosdk.util import algos_to_microalgos
from algosdk.abi import ABIType
from dotenv import load_dotenv

load_dotenv()

algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

path = Path(__file__).parent / './StakingContract.arc32.json'
app_id = int(os.getenv('app_id'))
private_key = os.getenv('private_key')
signer = AccountTransactionSigner(private_key)
address = address_from_private_key(private_key)
params = algod_client.suggested_params()

app_client = ApplicationClient(
    algod_client=algod_client,
    app_spec=path,
    app_id=app_id,
    signer=signer,
    sender=address,
    suggested_params=params,
)

atc = AtomicTransactionComposer()

address_coder = ABIType.from_string('(address)')
box_key = address_coder.encode((address,))

stake_algo_transaction = PaymentTxn(sender=address, sp=params,receiver=app_client.app_address,amt=algos_to_microalgos(0.1))
stake_with_signer_tx = TransactionWithSigner(stake_algo_transaction, signer)

fee_payment_txn = PaymentTxn(sender=address, sp=params,receiver=app_client.app_address,amt=21700)
fee_payment_with_signer_tx = TransactionWithSigner(fee_payment_txn, signer)

app_client.compose_call(atc, call_abi_method='stake',pay_amount=stake_with_signer_tx, fee_payment=fee_payment_with_signer_tx, transaction_parameters={'boxes': [[app_id, box_key]]})

result = atc.execute(algod_client, 2)
print(result.tx_ids[0])
print(result.abi_results[0].return_value)

Claiming Reward and Withdrawing from Contract

from algokit_utils import ApplicationClient
from algosdk.v2client.algod import AlgodClient
from algosdk.atomic_transaction_composer import AccountTransactionSigner, AtomicTransactionComposer
from algosdk.account import address_from_private_key
from algosdk.abi import ABIType
from pathlib import Path
import os
from dotenv import load_dotenv

load_dotenv()

algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

path = Path(__file__).parent / './StakingContract.arc32.json'
app_id = int(os.getenv('app_id'))
private_key = os.getenv('private_key')
signer = AccountTransactionSigner(private_key)
address = address_from_private_key(private_key)
params = algod_client.suggested_params()

app_client = ApplicationClient(
    algod_client=algod_client,
    app_spec=path,
    app_id=app_id,
    signer=signer,
    sender=address,
    suggested_params=params,
)

atc = AtomicTransactionComposer()

address_coder = ABIType.from_string('(address)')
box_key = address_coder.encode((address,))

app_client.compose_call(atc, call_abi_method='claimRewardAndWithdraw', transaction_parameters={'boxes': [[app_id, box_key]]})

result = atc.execute(algod_client, 2)
print(result.tx_ids[0])
print(result.abi_results[0].return_value)

Code Editor