Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Article Thumbnail

Django Staking Service Demo

Introduction

This article introduces you to an example staking platform that’s available on github, to demonstrate how such a service can be created on Algorand. Please note that this example has not been audited and should not be considered ready for production. It is purely for educational purposes to help show how a platform can be constructed from end-to-end. If you’d like to see a video demonstration of the staking demo first, checkout the YouTube video below.

Before we jump into using the front end or learning about the smart contract functionality, let me explain a little more about what this demo offers. The staking pools have been designed to be as simple as possible, removing any need for an external oracle or using any complex mathematics. You’re encouraged to extend the capabilities of the smart contracts to continue learning how it impacts the user experience.

The staking asset and the reward asset can be the same asset if desired, but throughout this article we make a clear distinction between them by setting them as two separate assets. The value of these assets are assumed to be valued 1:1, so you would need to implement an oracle price lookup if you intend for rewards to be given at a different ratio.

Whilst it’s entirely possible to replace the wallet with Pera Wallet, this demo has been created with AlgoSigner, so please bear in mind that you will need to use a Chrome browser with the AlgoSigner extension installed to interact with the front end. If you just want to use the smart contracts you can always use the provided instructions at the bottom, or use the deploy.sh bash script which runs through a full use of the contract.

Front End

For this demo the popular Python web framework Django has been used to create a frontend for users to interact with the staking pool smart contracts. Extensive use of the py-algorand-sdk has been used to construct transactions and interact with an algod node.

Once you launch the django server (python3 manage.py runserver), you can visit the website at http://127.0.0.1:8000/. Here you will see the “View Pools” page, which will be blank the first time you view it. Note that AlgoSigner will likely popup asking you to grant access. It’s coded to look for and use a “sandnet-v1” ledger, so make sure your AlgoSigner is configured to communicate with your sandbox environment. You can find instructions on how to add a new network in the AlgoSigner docs.

EditorImages/2022/06/27 12:05/Screenshot_2022-06-13_at_10.06.22.png

Create Asset

As part of the demo, a simple asset creation page has been created to help you quickly create different test assets to play around with the staking pools. For this article go ahead and create 1 “staking asset” and 1 “reward asset”. The full supply of the reward asset will be sent to the staking pool when deploying the pool.
Note: The majority of testing has been done using 6 decimals and 1,000,000,000,000 total supply, so if you notice any issues using other values please feel free to identify, fix, and issue a PR to the github repository.

EditorImages/2022/06/27 12:21/Screenshot_2022-06-13_at_10.17.45.png

Create Pool

Once you’ve created your two assets, you’re ready to create a new staking pool. Use the asset IDs to indicate which asset is the staking one and which is the reward one. Whilst testing I would recommend using a large fixed rate percentage for the staking pool, so you can see numbers incrementing rapidly without needing to check back after a few hours. Typically 100% or 1000% has been used when developing this frontend. The begin and end fields are required to be formatted as unix times and should be set to somewhen in the future, as you cannot deploy a pool that has already begun or ended. During development I’d often create it with the beginning 5 minutes in the future, and have it end 1 hour later. Note that the option to withdraw everything from the pool is only available on the interface once the end has lapsed.

EditorImages/2022/06/27 12:21/Screenshot_2022-06-13_at_10.19.36.png

When you first create the pool you will be presented a transaction to review, approve, and sign. This is the initial deployment transaction which puts the staking pool onto the chain. There is then a second stage which consists of 4 transactions, which are funding the minimum balance requirement, initialising the staking pool (opting into the assets), providing the reward assets, and setting the fixed rate of return. They are all grouped together in this demo, but could be split into two further stages of initialising, and funding rewards.

EditorImages/2022/06/27 12:22/Screenshot_2022-06-13_at_10.20.31.png

View Pools

You will now see your new staking pool listed on the front page. You can view more details about it by clicking on the pool ID. Under the “Details” column you will see the start date, or the time remaining, or “ended” if it’s finished.

EditorImages/2022/06/27 12:22/Screenshot_2022-06-13_at_10.20.50.png

View Pool

Now you’re viewing the staking pool, you can see the interface to deposit, withdraw, or claim rewards. You may deposit the staking asset prior to the start of the staking pool, but rewards won’t accrue until after it has begun.

EditorImages/2022/06/27 12:23/Screenshot_2022-06-13_at_10.21.10.png

Once the beginning time has passed, the rewards will automatically start going up based on the size of the stake and duration. The javascript on the page will perform the same calculation to match what the smart contract would provide when evaluated. At this point you can use “Claim Rewards” to receive the assets into your account, or you can add to or remove from your staked position.

EditorImages/2022/06/27 12:23/Screenshot_2022-06-13_at_10.34.43.png

Smart Contracts

The original smart contracts were created using TEAL and can be found in the ./staking/contracts/ directory of the repository. Whilst TEAL isn’t everyone’s preferred choice, the comments should help explain how the contract works if you want to look into them. If you’d prefer to see a PyTeal version of the contract, there is one in the same directory in the file contracts.py. You will have to run it and produce the TEAL output. Additionally this PyTeal version will also generate an ARC4 compliant ABI json file.

There is only one smart contract which is used per staking pool. The process to create a new staking pool happens in multiple steps but can be consolidated into 2 main stages. The deployment stage and the initialisation stage.

The following sections will guide you through deploying a staking pool via the command line as opposed to using the UI described earlier in the article. You are encouraged to use a sandbox environment on a sandnet-v1 network.

Deployment Stage

The first stage is to deploy the smart contract to the network, this can be done by anyone, however this demo project expects you to have a single authorised deployment manager which is the only account that deploys the staking pools for you. That way it’s easier to track and list the staking pools.

goal app method --create -f $DEPLOYER \
    --on-completion "NoOp" \
    --method "deploy(asset,asset,uint64,uint64)void" \
    --arg $STAKING_ASSET_ID \
    --arg $REWARD_ASSET_ID \
    --arg $BEGIN_TIMESTAMP \
    --arg $END_TIMESTAMP \
    --global-byteslices 1 --global-ints 10 \
    --local-byteslices 0 --local-ints 3 \
    --approval-prog staking.teal \
    --clear-prog clear.teal

Some notes about the deploy call. The beginning timestamp ($BEGIN_TIMESTAMP) of the pool must be in the future.

Initialisation Stage

The second stage interacts with the deployed smart contract with two goals in mind. The first is to fund the smart contract with the minimum balance requirement needed to opt in to the ASAs used for staking and rewards. The second is to provide the reward assets to the smart contract and set the reward rate, which for this example is a fixed rate APR.

To fund the staking pool account with the minimum balance requirement, you can look up the application info using goal app info –app-id $APP_ID. Create and save the payment transaction to a file which can be used for the method called to initialise the staking pool.

goal clerk send -f $DEPLOYER \
    -t $APP_ADDR \
    -a 302000 \
    -o minbal.txn


goal app method --app-id $APP_ID -f $DEPLOYER \
    --on-completion "NoOp" \
    --method "init(pay,asset,asset)void" \
    --arg minbal.txn \
    --arg $STAKING_ASSET_ID \
    --arg $REWARD_ASSET_ID

Next we provide the staking pool with the rewards and set the fixed rate APR. This is done in the same way as the previous transactions, where we save an asset transfer transaction to a file and then use a method call to the app, including the asset transfer. When performing this step in the Django UI, be aware that the entire total supply of the reward asset is provided as default.

goal asset send --assetid $REWARD_ASSET_ID -f $DEPLOYER \
    -t $APP_ADDR \
    -a $REWARD_TOTAL \
    -o rewards.txn

Notice that the reward rate (represented in basis points) is set to 10,000 (i.e. 100.00%) APR. This means if we deposit 1,000 staking assets, after 1 year we’d have 1,000 reward assets available to claim.

goal app method --app-id $APP_ID -f $DEPLOYER \
    --on-completion "NoOp" \
    --method "reward(axfer,uint64,asset)void" \
    --arg rewards.txn \
    --arg 10000 \
    --arg $REWARD_ASSET_ID

You’ve now successfully created a staking pool, initialised it, and funded it with the rewards. Once the staking pool begin timestamp has been reached any staked assets will begin accruing rewards at the fixed rate defined.

Post Deployment Interactions

Once you have a deployed staking pool it can be interacted with. For typical users this will include, depositing and withdrawing the staking asset, and also claiming their rewards. For the Admin account this includes config and update. The admin’s init and reward interactions are covered above during deployment.

Deposit

Now with a fully deployed and funded staking pool we will deposit some staking asset into it as a typical user. Allowing the rewards to accrue over time. First we create the asset transfer transaction and save it to a file to be used in the method call as an argument. With an assumed 6 decimals, we’re sending 10.0 of the staking asset into the pool.

goal asset send -t $APP_ADDR -f $USER \
    --assetid $STAKING_ASSET_ID \
    -a 10000000 \
    -o deposit.txn


goal app method --app-id $APP_ID -f $USER \
    --on-completion "OptIn" \
    --method "deposit(axfer,asset)void" \
    --arg deposit.txn \
    --arg $STAKING_ASSET_ID

Withdraw

Similar to how a user can deposit an amount of staking asset into the pool, users can withdraw their assets at any time. This is because there is no locking mechanism on the staking pool. Following on from the above deposit of 10 staking assets, we will withdraw half of them now.

goal app method --app-id $APP_ID -f $USER \
    --on-completion "NoOp" \
    --method "withdraw(asset,uint64,account)void" \
    --arg $STAKING_ASSET_ID \
    --arg 5000000 \
    --arg $USER \
    --fee 2000

Depending on the length of time that passed since the original deposit, an amount of rewards may have already accumulated and will have been credited to your local state. Rewards may also be withdrawn from the staking pool at any time, in full or partially. We’ll demonstrate this next.

Claiming Rewards

To claim the rewards an account has accrued, the process is almost identical to the withdrawal process. You just replace the staking asset id ($STAKING_ASSET_ID) with the reward asset id ($REWARD_ASSET_ID), and the amount you wish to pull down. Another feature of the smart contract is that if you specify an amount larger than you have available, it will provide you with the maximum amount you’re entitled to, which includes any newly calculated rewards that may have accrued since you last checked or since submitting the transaction. It is also possible to withdraw the assets to a different account other than the one who earned them, by specifying a different account within the arguments.

Below we claim all the available rewards by specifying the largest uint64 value possible.

goal app method --app-id $APP_ID -f $USER \
    --on-completion "CloseOut" \
    --method "withdraw(asset,uint64,account)void" \
    --arg $REWARD_ASSET_ID \
    --arg 18446744073709551615 \
    --arg $USER \
    --fee 2000

Config (Admin)

The config method is available so that you can manage the smart contract beyond the original deployer account. This may be necessary if you wish to update from the original deployer account to some sort of multisig account. The contract also allows the admin to “pause” the deposits and withdrawals in the event of a vulnerability being discovered. Once paused the smart contract can be updated. None of these features are explained in detail, so it’s left up to the reader to find out more about how they work.

Miscellaneous Details

In this section I’ll explain different parts of the smart contract or demo design and how they work. There isn’t any order to them, but they’re important enough that I wanted to add them in the event you search the article for a particular subject.

Calculate Reward

This subroutine is called every time an interaction with a user is made. Whether they deposit, withdraw, or claim their rewards we want to calculate what rewards they’re entitled to prior to any changes they’re about to make.

The calculation uses the following formula:
Amount Staked * Duration Since Last Interaction / 31,557,600 * Fixed Rate / 10,000
31,557,600 is the average number of seconds in a year.
10,000 is used to calculate the percentage from basis points.

The equivalent calculation performed in PyTeal is shown below.

@Subroutine(TealType.none)
def calculate_rewards(addr: Expr) -> Expr:
    return Seq(
        # Skip if not begun
        If(Global.latest_timestamp() > App.globalGet(Bytes("BT")), Return()),

        # Skip if updated since ET
        If(App.localGet(addr, Bytes("LU")) < App.globalGet(Bytes("ET")), Return()),

        # Calculate time since last update
        # End
        (end := ScratchVar()).store(
            If(Global.latest_timestamp() > App.globalGet(Bytes("ET")))
            .Then(App.globalGet(Bytes("ET")))
            .Else(Global.latest_timestamp())
        ),
        # Start
        (start := ScratchVar()).store(
            If(App.localGet(addr, Bytes("LU")) < App.globalGet(Bytes("BT")))
            .Then(App.globalGet(Bytes("BT")))
            .Else(App.localGet(addr, Bytes("LU")))
        ),
        # Duration
        (duration := ScratchVar()).store(end.load() - start.load()),

        # Calculate time since last updated
        (rewards := ScratchVar()).store(
            App.localGet(addr, Bytes("AS")) * duration.load() / Int(31557600) * App.globalGet(Bytes("FR")) / Int(10000)
        ),

        # Remove rewards from global
        App.globalPut(Bytes("TR"), App.globalGet(Bytes("TR")) - rewards.load()),

        # Add rewards to local
        App.localPut(addr, Bytes("AR"), App.localGet(addr, Bytes("AR")) + rewards.load()),
    )

Breakdown of Deploy PyTeal

For more details on how you can write ARC4 compliant PyTeal smart contracts refer here. Below is the deploy method from the PyTeal staking smart contract, in this section I will briefly cover the different parts to help understand how a transaction would be constructed for this method.

Starting with the router decorator at the top, this tells PyTeal that this will be an ABI method that will be accessible by supplying a method selector as argument 0, and will only be available when making a NoOp application call to create the smart contract.

The arguments we need to pass in during the application call transaction are provided as arguments to the method function. These include two assets, and two timestamps (as uint64s). The first two assets are reference types and as such will have their values placed within the transactions asset reference array and the latter two are actual application arguments which get automatically converted from bytes to uint64s.

@router.method(no_op=CallConfig.CREATE)
def deploy(
    staking: abi.Asset,
    reward: abi.Asset,
    begin: abi.Uint64,
    end: abi.Uint64,
) -> Expr:
    """Used to deploy the contract, defining assets and times."""

Now for the actual logic of the method. We use a single Seq() expression that will evaluate each of our individual expressions one-by-one. We check that there is no application ID supplied with the call (Note that this check is largely redundant because of how the router was instructed to use CallConfig.CREATE as the no_op argument, which does this check before getting here. Next we set the “admin” of our contract as the account deploying the contract. This becomes important when administering the contract after deployment.

    return Seq(
        # Can only deploy as a new smart contract.
        Assert(Not(Txn.application_id())),

        # User sender as admin.
        set_admin(Txn.sender()),

Finally we set the staking, reward, begin, and end times for the contract, whilst checking the timestamps are valid (beyond the latest timestamp, and increasing in time). Once this has finished, we successfully end the evaluation and the transaction is approved.

        # Set staking asset
        App.globalPut(Bytes("SA"), staking.asset_id()),

        # Set reward asset
        App.globalPut(Bytes("RA"), reward.asset_id()),

        # Set begin timestamp
        # Must be after LatestTimestamp
        Assert(Gt(begin.get(), Global.latest_timestamp())),
        App.globalPut(Bytes("BT"), begin.get()),

        # Set end timestamp
        # Must be after begin timestamp
        Assert(Gt(end.get(), begin.get())),
        App.globalPut(Bytes("ET"), end.get()),

        # Success
        Approve(),
    )

ABI JSON File

The contract’s ABI JSON file can be used by ARC4 compliant software to quickly and easily identify what methods are available to be called within a smart contract. Well crafted PyTeal contracts will allow you to automatically generate this file at the same time as the TEAL. But if you write your contracts using TEAL, you’ll likely have to create this file by hand.

{
  "name": "staking",
  "methods": [
    {
      "name": "deposit",
      "args": [
        {
          "type": "axfer",
          "name": "axfer"
        },
        {
          "type": "asset",
          "name": "asset"
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Deposit adds an amount of staked assets to the pool, increasing the senders share of the rewards."
    },
    {
      "name": "withdraw",
      "args": [
        {
          "type": "asset",
          "name": "asset"
        },
        {
          "type": "uint64",
          "name": "amount"
        },
        {
          "type": "account",
          "name": "recipient"
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Remove an amount of staked assets or reward assets from the pool."
    },
    {
      "name": "deploy",
      "args": [
        {
          "type": "asset",
          "name": "staking"
        },
        {
          "type": "asset",
          "name": "reward"
        },
        {
          "type": "uint64",
          "name": "begin"
        },
        {
          "type": "uint64",
          "name": "end"
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Used to deploy the contract, defining assets and times."
    },
    {
      "name": "init",
      "args": [
        {
          "type": "pay",
          "name": "pay"
        },
        {
          "type": "asset",
          "name": "staking"
        },
        {
          "type": "asset",
          "name": "reward"
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Initialise the newly deployed contract, funding it with a minimum balance and allowing it to opt in to the request assets."
    },
    {
      "name": "reward",
      "args": [
        {
          "type": "axfer",
          "name": "rewards"
        },
        {
          "type": "uint64",
          "name": "fixed_rate"
        },
        {
          "type": "asset",
          "name": "reward"
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Primarily used to supply the initial rewards for the staking contract, but can also be used to add additional rewards before the contract ends."
    },
    {
      "name": "config",
      "args": [
        {
          "type": "bool",
          "name": "paused"
        },
        {
          "type": "account",
          "name": "admin"
        }
      ],
      "returns": {
        "type": "void"
      }
    }
  ],
  "desc": null,
  "networks": {}
}