Skip to main content
Developers
Omnichain Contracts
Tutorials
Staking

Staking

In this tutorial you will create an omnichain contract that will be capable of receiving tokens from connected chains and staking them on ZetaChain. Native tokens deposited to ZetaChain as ZRC-20s will be locked in the contract until withdrawn by the staker. Rewards will be accrued at a fixed rate proportionally to amount of tokens staked.

The staker is the one depositing tokens to the contract. The staker is required to provide a beneficiary address to which the rewards will be sent (a staker is allowed to be its own beneficiary). Only the staker can withdraw the staked tokens from the contract and withdraw to the chain from which they originate.

Only the beneficiary can withdraw the rewards from the contract.

For simplicity this contract will be compatible with one of the chain of your choosing. The chain ID of the compatible connected chain is defined by the deployer of the contract.

This tutorial demonstrates:

  • how to create an omnichain contract that executes logic with tokens from a connected chain
  • how to use the parameters of the onCrossChainCall function to:
    • get the staker address from the context parameter
    • get the beneficiary address from the message parameter both for EVM-based chains and for Bitcoin
  • how to withdraw tokens correctly both to EVM-based chains and to Bitcoin

Prerequisites

Set Up Your Environment

Clone the Hardhat contract template:

git clone https://github.com/zeta-chain/template

Install dependencies:

cd template
yarn

Create the Contract

To create a new omnichain contract you will use the omnichain Hardhat task and pass a contract name (Staking) and one argument (beneficiary) to the task:

npx hardhat omnichain Staking beneficiary:address

The beneficiary of type address argument will be provided by users calling the omnichain contract from one of the connected chains. The beneficiary argument will be passed to the onCrossChainCall function in the message parameter.

Omnichain Contract

Let's review the contents of the MyContract contract:

contracts/Staking.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "@zetachain/protocol-contracts/contracts/zevm/SystemContract.sol";
import "@zetachain/protocol-contracts/contracts/zevm/interfaces/zContract.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@zetachain/toolkit/contracts/BytesHelperLib.sol";

contract Staking is ERC20, zContract {
error SenderNotSystemContract();
error WrongChain();

SystemContract public immutable systemContract;
uint256 public immutable chainID;

constructor(
string memory name_,
string memory symbol_,
uint256 chainID_,
address systemContractAddress
) ERC20(name_, symbol_) {
systemContract = SystemContract(systemContractAddress);
chainID = chainID_;
}

function onCrossChainCall(
zContext calldata context,
address zrc20,
uint256 amount,
bytes calldata message
) external override {
if (msg.sender != address(systemContract)) {
revert SenderNotSystemContract();
}

address acceptedZRC20 = systemContract.gasCoinZRC20ByChainId(chainID);
if (zrc20 != acceptedZRC20) revert WrongChain();

address staker = BytesHelperLib.bytesToAddress(context.origin, 0);
address beneficiary;

if (context.chainID == 18832) {
beneficiary = BytesHelperLib.bytesToAddress(message, 0);
} else {
beneficiary = abi.decode(message, (address));
}

stakeZRC(staker, beneficiary, amount);
}

}