About Smart Contract

If you are a smart contract developer, you will need to consider something about the contract: 

  • Clean: No unused code, nonsense comment, 
  • Well document: Comment to all functions
  • Formatted: Reformat, reformat, reformat
  • Unit test all functions
  • Migration for testnet and mainnet
  • Avoid duplication code, avoid using too many unnecessary modifiers to reduce contract size
  • Upgradable

*Reusable, Replaceable, Upgradable, Testable => Easier maintenance*

— — — 

About some checklist in contract function

  • Consider using nonReentrant modifier in paying function to avoid duplicate calls
  • Use safe math to check about substract and div operators. Get revert if you div zero, or assign a negative number to uint.
  • About percent calculation should use the rate nominator approach. MUL first, DIV last
uint internal constant RATE_NOMINATOR = 10000;
uint tax = 100; // 1%
uint taxAmount = (amount * tax) / RATE_NOMINATOR;
  • Do not loop more than 500 in an array, use pagination for long array

 — — — 

About how to write code and unit tests

  • You need to clear the requirement first
  • Define test cases in your note
  • Write contract interface with main functions
  • Write unit test case for contract interface functions -> obviously test will fail this time
  • Write implementation to pass the test cases. 
  • Need to complete each function one by one, do not put a lot of code then check -> it will be messy and you may don’t know where is the bug

 — — — 

About upgradable thing

Depend on the requirement, project scope, you need to analyze it to choose an upgradeable approach corresponding

How can upgrade contract logic but keep the same contract address?

Using Openzepplin proxy approach: https://docs.openzeppelin.com/learn/upgrading-smart-contracts

But it also has some limitations: https://docs.openzeppelin.com/learn/upgrading-smart-contracts#limitations-of-contract-upgrades

  • Don’t have the constructor
  • Cannot use read/write a contract on the scan URL (bscscan/etherscan). Need programmatically with the contract abi
  • Cannot remove the old state from the previous version, mean the contract may increase a lot in code size and redundant state in further

If the current contract version is not upgradeable.

You need to handle multi-contract versions at your application level: Frontend and Backend.

If you don’t like the Openzepplin proxy approach

  • Consider with some patterns: repository pattern, mvp/mvc. 
  • You should choose the pattern that separated the store/model out of view and logic -> you can deploy a new version and migrate data then re-link it. 
  • Below is the pattern for Dividend Token Upgradable
Dividend Token Upgradable
Dividend Token Upgradable

We have 2 contracts: ERC20 token and Token Presenter. As default token works as normally token without presenter, if you have business logic later, you can implement to presenter then attach to the token. 

ERC20Token.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.3;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./interfaces/ITokenPresenter.sol";

/**
ERC20Token implementation
 */
contract ERC20Token is ERC20, Ownable {

  address public presenter;
  uint8 private _decimals;

  constructor(string memory name_, string memory symbol_, uint8 decimals_, uint256 initialSupply_) ERC20(name_, symbol_) {
    _decimals = decimals_;
    _mint(_msgSender(), initialSupply_ * 10 ** uint256(decimals_));
  }

  /**
  * @dev set the decimal
  */
  function decimals() override public view returns (uint8) {
    return _decimals;
  }

  /**
  * @dev set the presenter of the token to decide transfer functionality
  * @param _presenter address of presenter
  */
  function setPresenter(address _presenter) onlyOwner public {
    presenter = _presenter;
  }

  /**
  * @dev transfer the tokens, if presenter is not set, normal behaviour
  */
  function _transfer(address _from, address _to, uint256 _amount) internal override {
    // Transfer fund and responsibility to presenter
    if (presenter != address(0) && presenter != _msgSender()) {
      super._transfer(_from, presenter, _amount);
      ITokenPresenter(presenter).receiveTokens(_msgSender(), _from, _to, _amount);
    } else {
      super._transfer(_from, _to, _amount);
    }
  }

}

TokenPresenter.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.3;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./interfaces/ITokenPresenter.sol";
import "./utils/Maintainable.sol";

contract TokenPresenter is ITokenPresenter, Maintainable {

address public token;

event Received(address sender, uint amount);

constructor() {
token = address(0);
}

/**
* @dev allow contract to receive ethers
*/
receive() external payable {
emit Received(msg.sender, msg.value);
}

/**
* @dev set the main token
* @param _token address of main token
*/
function setToken(address _token) onlyOwner public {
token = _token;
}

/**
* @dev this is the main function to distribute the tokens call from only main token
* @param _from from address
* @param _to to address
* @param _amount amount of tokens
*/
function receiveTokens(address _from, address _to, uint256 _amount) public override returns (bool) {
return receiveTokensFrom(_from, _from, _to, _amount);
}

/**
* @dev this is the main function to distribute the tokens call from only main token via external app
* @param _trigger trigger address
* @param _from from address
* @param _to to address
* @param _amount amount of tokens
*/
function receiveTokensFrom(address _trigger, address _from, address _to, uint256 _amount) public override returns (bool) {
ifNotMaintenance();
require(msg.sender == token, "TokenPresenter: Only trigger from token");
IERC20(token).transfer(_to, _amount);
return true;
}

/**
* @dev get the eth balance on the contract
* @return eth balance
*/
function getEthBalance() public view onlyOwner returns (uint) {
return address(this).balance;
}

/**
* @dev withdraw eth balance
*/
function withdrawEthBalance() external onlyOwner {
payable(owner()).transfer(getEthBalance());
}

/**
* @dev get the token balance
* @param _tokenAddress token address
*/
function getTokenBalance(address _tokenAddress) public view onlyOwner returns (uint) {
IERC20 erc20 = IERC20(_tokenAddress);
return erc20.balanceOf(address(this));
}

/**
* @dev withdraw token balance
* @param _tokenAddress token address
*/
function withdrawTokenBalance(address _tokenAddress) external onlyOwner {
IERC20 erc20 = IERC20(_tokenAddress);
erc20.transfer(
owner(),
getTokenBalance(_tokenAddress)
);
}
}

Maintainable.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;

import "@openzeppelin/contracts/access/Ownable.sol";

contract Maintainable is Ownable {

bool public isMaintenance = false;
bool public isOutdated = false;

// Check if contract is not in maintenance
function ifNotMaintenance() internal view {
require(!isMaintenance, "Maintenance");
require(!isOutdated, "Outdated");
}

// Check if contract on maintenance for restore
function ifMaintenance() internal view {
require(isMaintenance, "!Maintenance");
}

// Enable maintenance
function enableMaintenance(bool status) onlyOwner public {
isMaintenance = status;
}

// Enable outdated
function enableOutdated(bool status) onlyOwner public {
isOutdated = status;
}

}

ITokenPresenter.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.3;

interface ITokenPresenter {
  function receiveTokens(address _trigger, address _from, address _to, uint256 _amount) external returns (bool);
}

You need to add a “restore” function like this for upgrade

ContractV1 is Maintainable {
 uint a = 1;

}

ContractV2 is Maintainable {
 uint a;
 restore(address contractV1) public onlyOwner ifMaintenance {
  a = ContractV1(contractV1).a;
 }
}

====
UPGRADE FLOW:
- ContractV1 -> enableMaintenance(true)
- ContractV2 -> enableMaintenance(true)
- ContractV2 -> restore(contract v1 address)
- ContractV2 -> enableMaintenance(false)
- ContractV1 -> enableOutdated(true)

You also need to define an array to store all users in the contract not only map. 

To export data from array need implement pagination, we can write a js script to export and run migrate user data each page.

Leave a Reply

Your email address will not be published.Required fields are marked *