tags: Final Report

STEPN Audit

Copyright © 2022 by Verilog Solutions. All rights reserved.
April 4, 2022
by Verilog Solutions

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

This report presents our engineering engagement with STEPN, a Game-Fi/Social-Fi Web3 application. Users could acquire STEPN NFT sneakers and earn rewards by engaging in outdoor activities. STEPN tokens have two tokens: GST & GMT.


Table of Content


Project Summary

STEPN is a Game-Fi/Social-Fi project with a dual-token system (GMT and GST) and an NFT system (STEPN Sneaker). Users acquire the STEPN Sneaker to participate in the move-and-earn program and earn GST and/or GMT. GMT and GST can be used to upgrade Sneakers and increase the rate of earning. GMT is the governance token, and it is currently deployed on Solana and BNB Chain. GST is the unlimited-supply reward token, and it is currently deployed on Solana.


Service Scope

Our review focused on the main branch, specifically, commit hash c07ff86e5e8060de0cd10d6842405964c2cc5d13.

Our auditing service for STEPN includes the following two stages:

  • Pre-Audit Consulting Service
  • Audit Service
  1. Pre-Audit Consulting Service

    As a part of the pre-audit service, the Verilog Solutions team worked closely with the STEPN development team to discuss potential vulnerability and smart contract development best practices in a timely fashion. Verilog Solutions team is very appreciative of establishing an efficient and effective communication channel with the STEPN team, as new findings are often exchanged promptly and fixes were deployed quickly, during the preliminary report stage.

  2. Audit Service

    The Verilog Solutions team conducted a thorough study of the STEPN code. The list of findings, along with the severity and solution, is available under the section Findings & Improvement Suggestions.


GMT & GST Token

Below is the summary of GMT & GST token info:

Network Token Token Address
Solana GMT 7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx
Solana GST AFbX8oGjGpmVFywbVouvhQSRmiW2aR1mohfahi4Y2AdB
BNB GMT 0x3019BF2a2eF8040C242C9a4c5c4BD4C81678b2A1
BNB GST n/a (coming soon)

Deployment on Solana

STEPN dev team used Solana Token Program to create both GMT & GST tokens. Token Program defines a common implementation for Fungible and Non Fungible tokens.

The document of the Token Program: https://spl.solana.com/token
The Github of Token Program: https://github.com/solana-labs/solana-program-library

As can be seen from the Solscan, both GMT & GST tokens used Solana Token Program:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Besides, the STEPN project dev team already turned off the emission right for GMT Token as can be seen in the screenshot, GMT token has a fixed supply.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Deployment on BNB Chain

Currently, only the GMT token has been deployed on BNB Chain, and the token has been implemented by the Binance Bridge team. The contract deployment address is 0x3019BF2a2eF8040C242C9a4c5c4BD4C81678b2A1.

Below is the summary of GMT token on BNB Chain:

Title Info
Network BNB Chain
Token GMT
Upgradable Contract? Yes
Proxy Address 0x3019BF2a2eF8040C242C9a4c5c4BD4C81678b2A1
Implementation Address 0xba5fe23f8a3a24bed3236f05f2fcf35fd0bf0b5c

Implementation Contract Source Code can be found in GMT BNB Implementation.
In summary, the implemented smart contracts follow the ERC20 standards.


Privileged Roles

  1. GreenSatoshiToken.sol:
    a. Owner can mint() any amount of GST tokens to any address without limitations.

  2. STEPNNFT.sol:
    a. Owner can setBaseURI(), mint().


Findings & Improvement Suggestions

InformationalMinorMediumMajorCritical

Total Acknowledged Resolved
Critical 0 0 0
Major 0 0 0
Medium 2 2 0
Minor 0 0 0
Informational 0 0 0

Critical

none ;)

Major

none ;)

Medium

  1. Centralization Risks on GreenSatoshiToken.sol. Medium

    ​​​​function mint(address to, uint256 amount) public onlyOwner { ​​​​ _mint(to, amount); ​​​​}

    Description: Owner of this smart contract can mint tokens to certain addresses, Private key leaks may result in the unlimited token supply issue.
    Recommendation: uses a multisig wallet to prevent a single point of failure.
    Feedback from Project Team: As disclosed in the STEPN whitepaper, GST has an unlimited supply therefore we have to enable the function to mint an unlimited amount of GST.

  2. Centralization Risks on STEPNNFT.sol. Medium

    ​​​​function setBaseURI(string memory buri) public onlyOwner { ​​​​ require(bytes(buri).length > 0, "wrong base uri"); ​​​​ _buri = buri; ​​​​} ​​​​function mint(address to, uint256 tokenId) public onlyOwner { ​​​​ _safeMint(to, tokenId); ​​​​}

    Description: Owner of this smart contract can mint tokens to certain addresses, Private key leaks may result in the unlimited NFT supply issue. Owner of this smart contract can change the base URL of the NFT.
    Recommendation: uses multisig wallet to prevent a single point of failure.
    Feedback from Project Team: STEPN’s NFT sneaker also has an unlimited supply, therefore we have to beagle the function to allow an unlimited amount of NFT sneakers to be minted by our users.

Minor

none ;)

Informational

none ;)

Reference Code

In this section, we listed the deployed contract on-chain for your reference.

GMT BNB Chain Implementation Code

contract BEP20TokenImplementation is Context, IBEP20, Initializable { using SafeMath for uint256; mapping (address => uint256) private _balances; mapping (address => mapping (address => uint256)) private _allowances; uint256 private _totalSupply; string private _name; string private _symbol; uint8 private _decimals; address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); bool private _mintable; constructor() public { } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { require(_owner == _msgSender(), "Ownable: caller is not the owner"); _; } /** * @dev sets initials supply and the owner */ function initialize(string memory name, string memory symbol, uint8 decimals, uint256 amount, bool mintable, address owner) public initializer { _owner = owner; _name = name; _symbol = symbol; _decimals = decimals; _mintable = mintable; _mint(owner, amount); } /** * @dev Leaves the contract without the owner. It will not be possible to call * `onlyOwner` functions anymore. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, * thereby removing any functionality that is only available to the owner. */ function renounceOwnership() public onlyOwner { emit OwnershipTransferred(_owner, address(0)); _owner = address(0); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Can only be called by the current owner. */ function transferOwnership(address newOwner) public onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); emit OwnershipTransferred(_owner, newOwner); _owner = newOwner; } /** * @dev Returns if the token is mintable or not */ function mintable() external view returns (bool) { return _mintable; } /** * @dev Returns the bep token owner. */ function getOwner() external override view returns (address) { return _owner; } /** * @dev Returns the token decimals. */ function decimals() external override view returns (uint8) { return _decimals; } /** * @dev Returns the token symbol. */ function symbol() external override view returns (string memory) { return _symbol; } /** * @dev Returns the token name. */ function name() external override view returns (string memory) { return _name; } /** * @dev See {BEP20-totalSupply}. */ function totalSupply() external override view returns (uint256) { return _totalSupply; } /** * @dev See {BEP20-balanceOf}. */ function balanceOf(address account) external override view returns (uint256) { return _balances[account]; } /** * @dev See {BEP20-transfer}. * * Requirements: * * - `recipient` cannot be the zero address. * - the caller must have a balance of at least `amount`. */ function transfer(address recipient, uint256 amount) external override returns (bool) { _transfer(_msgSender(), recipient, amount); return true; } /** * @dev See {BEP20-allowance}. */ function allowance(address owner, address spender) external override view returns (uint256) { return _allowances[owner][spender]; } /** * @dev See {BEP20-approve}. * * Requirements: * * - `spender` cannot be the zero address. */ function approve(address spender, uint256 amount) external override returns (bool) { _approve(_msgSender(), spender, amount); return true; } /** * @dev See {BEP20-transferFrom}. * * Emits an {Approval} event indicating the updated allowance. This is not * required by the EIP. See the note at the beginning of {BEP20}; * * Requirements: * - `sender` and `recipient` cannot be the zero address. * - `sender` must have a balance of at least `amount`. * - the caller must have allowance for `sender`'s tokens of at least * `amount`. */ function transferFrom(address sender, address recipient, uint256 amount) external override returns (bool) { _transfer(sender, recipient, amount); _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "BEP20: transfer amount exceeds allowance")); return true; } /** * @dev Atomically increases the allowance granted to `spender` by the caller. * * This is an alternative to {approve} that can be used as a mitigation for * problems described in {BEP20-approve}. * * Emits an {Approval} event indicating the updated allowance. * * Requirements: * * - `spender` cannot be the zero address. */ function increaseAllowance(address spender, uint256 addedValue) public returns (bool) { _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); return true; } /** * @dev Atomically decreases the allowance granted to `spender` by the caller. * * This is an alternative to {approve} that can be used as a mitigation for * problems described in {BEP20-approve}. * * Emits an {Approval} event indicating the updated allowance. * * Requirements: * * - `spender` cannot be the zero address. * - `spender` must have allowance for the caller of at least * `subtractedValue`. */ function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) { _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue, "BEP20: decreased allowance below zero")); return true; } /** * @dev Creates `amount` tokens and assigns them to `msg.sender`, increasing * the total supply. * * Requirements * * - `msg.sender` must be the token owner * - `_mintable` must be true */ function mint(uint256 amount) public onlyOwner returns (bool) { require(_mintable, "this token is not mintable"); _mint(_msgSender(), amount); return true; } /** * @dev Burn `amount` tokens and decreasing the total supply. */ function burn(uint256 amount) public returns (bool) { _burn(_msgSender(), amount); return true; } /** * @dev Moves tokens `amount` from `sender` to `recipient`. * * This is internal function is equivalent to {transfer}, and can be used to * e.g. implement automatic token fees, slashing mechanisms, etc. * * Emits a {Transfer} event. * * Requirements: * * - `sender` cannot be the zero address. * - `recipient` cannot be the zero address. * - `sender` must have a balance of at least `amount`. */ function _transfer(address sender, address recipient, uint256 amount) internal { require(sender != address(0), "BEP20: transfer from the zero address"); require(recipient != address(0), "BEP20: transfer to the zero address"); _balances[sender] = _balances[sender].sub(amount, "BEP20: transfer amount exceeds balance"); _balances[recipient] = _balances[recipient].add(amount); emit Transfer(sender, recipient, amount); } /** @dev Creates `amount` tokens and assigns them to `account`, increasing * the total supply. * * Emits a {Transfer} event with `from` set to the zero address. * * Requirements * * - `to` cannot be the zero address. */ function _mint(address account, uint256 amount) internal { require(account != address(0), "BEP20: mint to the zero address"); _totalSupply = _totalSupply.add(amount); _balances[account] = _balances[account].add(amount); emit Transfer(address(0), account, amount); } /** * @dev Destroys `amount` tokens from `account`, reducing the * total supply. * * Emits a {Transfer} event with `to` set to the zero address. * * Requirements * * - `account` cannot be the zero address. * - `account` must have at least `amount` tokens. */ function _burn(address account, uint256 amount) internal { require(account != address(0), "BEP20: burn from the zero address"); _balances[account] = _balances[account].sub(amount, "BEP20: burn amount exceeds balance"); _totalSupply = _totalSupply.sub(amount); emit Transfer(account, address(0), amount); } /** * @dev Sets `amount` as the allowance of `spender` over the `owner`s tokens. * * This is internal function is equivalent to `approve`, and can be used to * e.g. set automatic allowances for certain subsystems, etc. * * Emits an {Approval} event. * * Requirements: * * - `owner` cannot be the zero address. * - `spender` cannot be the zero address. */ function _approve(address owner, address spender, uint256 amount) internal { require(owner != address(0), "BEP20: approve from the zero address"); require(spender != address(0), "BEP20: approve to the zero address"); _allowances[owner][spender] = amount; emit Approval(owner, spender, amount); } /** * @dev Destroys `amount` tokens from `account`.`amount` is then deducted * from the caller's allowance. * * See {_burn} and {_approve}. */ function _burnFrom(address account, uint256 amount) internal { _burn(account, amount); _approve(account, _msgSender(), _allowances[account][_msgSender()].sub(amount, "BEP20: burn amount exceeds allowance")); } }

GST Token Contract

STEPN team implemented a solidity version of GST token, which uses Openzeppelin's ERC20 standard libraries:

// contracts/GreenSatoshiToken.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; /** * Green Satoshi Token * @author STEPN */ contract GreenSatoshiToken is ERC20, ERC20Burnable, Ownable { constructor() ERC20("GreenSatoshiToken", "GST") {} function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } function decimals() public view virtual override returns (uint8) { return 8; } }

STEPNNFT nft contract

// contracts/STEPNNFT.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; /** * STEPN NFTs * @author STEPN */ contract STEPNNFT is ERC721Enumerable, Ownable { // base uri for nfts string private _buri; constructor() ERC721("STEPNNFT", "SNFT") {} function _baseURI() internal view override returns (string memory) { return _buri; } function setBaseURI(string memory buri) public onlyOwner { require(bytes(buri).length > 0, "wrong base uri"); _buri = buri; } function mint(address to, uint256 tokenId) public onlyOwner { _safeMint(to, tokenId); } function burn(uint256 tokenId) public virtual { require( _isApprovedOrOwner(_msgSender(), tokenId), "burn caller is not owner nor approved" ); _burn(tokenId); } }

Disclaimer

Verilog Solutions receives compensation from one or more clients for performing the smart contract and auditing analysis contained in these reports. The report created is solely for Clients and published with their consent. As such, the scope of our audit is limited to a review of code, and only the code we note as being within the scope of our audit detailed in this report. It is important to note that the Solidity code itself presents unique and unquantifiable risks since the Solidity language itself remains under current development and is subject to unknown risks and flaws. Our sole goal is to help reduce the attack vectors and the high level of variance associated with utilizing new and consistently changing technologies. Thus, Verilog Solutions in no way claims any guarantee of security or functionality of the technology we agree to analyze.

In addition, Verilog Solutions reports do not provide any indication of the technologies proprietors, business, business model, or legal compliance. As such, reports do not provide investment advice and should not be used to make decisions about investment or involvement with any particular project. Verilog Solutions has the right to distribute the Report through other means, including via Verilog Solutions publications and other distributions. Verilog Solutions makes the reports available to parties other than the Clients (i.e., “third parties”) – on its website in hopes that it can help the blockchain ecosystem develop technical best practices in this rapidly evolving area of innovation.