How to Mirror (Fork) Token Balances

written by wen_me

Choices, Choices, Choices…

As you know, there is no right path; there is only your path. Finding that path depends on what tools are available and what outcomes you hope to achieve.

Indexing Events/Merkle Root

You can index events on every transfer and create a Merkle root consisting of user addresses and user allocations.

To achieve this, you need an indexing service to make this information readily available. Or, you need to generate an exhaustive list of transfer events up to a certain block height, which may be resource intensive.

Pros:

  1. Gas-less
  2. Merkle Roots

Cons:

  1. Centralized indexing
  2. Centralized Merkle Root Creation

OpenZeppelin Snapshot

If you want to support the tracking of every transfer natively within your ERC20, simply include the ERC20Snapshot.sol ERC20 extension. When capturing current user balances, call captureSnapshot to reference the balances at a later point in time.

The downside of this approach is that it requires additional state management on every transfer function, which increases the gas costs for every transfer transaction.

Pros:

  1. Contract Layer Integration
  2. Decentralized

Cons:

  1. Expensive Creation & Activation

Each path has tradeoffs depending on whether the goal is decentralization or cost. This tutorial focuses on OpenZeppelin Snapshot.

Integrate OpenZeppelin ERC20Snapshot.sol

Prerequisites

  • ERC20 Knowledge
  • OZ Integrations / Smart Contracts
  • Hardhat

Let’s code

Create a contract called SnaphotToken.sol.

SnaphotToken.sol inherits from IERC20.sol and ERC20Snapshot.sol.

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";

contract SnapshotToken is IERC20, ERC20Snapshot {
  
}

Define the constructor and mint tokens to an array of holders.

constructor(
        string memory _name,
        string memory _symbol,
        address[] memory _hodlers,
        uint256[] memory _allocations
    ) ERC20(_name, _symbol) {
        for (uint256 i = 0; i < _hodlers.length; i++) {
            _mint(_hodlers[i], _allocations[i]);
        }
    }

Create an external function that calls the internal _snapshot function. This is the function that assigns user balances/total supply to a specific point in -time and may be referenced later when you are looking to fork/mirror token allocations.

 function captureSnapShot() external returns (uint256 snapId) {
        snapId = _snapshot();
    }

Finally, add the internal _beforeTokenTransfer function, which is called to update balance and/or total supply snapshots before the values are modified.

function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual override(ERC20Snapshot) {
        super._beforeTokenTransfer(from, to, amount);
    }

Review

If your code looks similar to the code below, you now have the ability to natively record users’ balances and quickly reference them by calling balanceOfAt(address account, uint256 snapshotID) or reference the totalSupply by calling totalSupplyAt(uint256 snapshotId).

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";

contract SnapshotToken is IERC20, ERC20Snapshot {
    constructor(
        string memory _name,
        string memory _symbol,
        address[] memory _hodlers,
        uint256[] memory _allocations
    ) ERC20(_name, _symbol) {
        for (uint256 i = 0; i < _hodlers.length; i++) {
            _mint(_hodlers[i], _allocations[i]);
        }
    }
    function captureSnapShot() external returns (uint256 snapId) {
        snapId = _snapshot();
    }
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual override(ERC20Snapshot) {
        super._beforeTokenTransfer(from, to, amount);
    }
}

Real-World Use Case

Let’s connect this alpha to a business case we have at Decent DAO.

Decent’s goal was to mirror the token holders’ balances which denote their voting power within a DAO to a subsidiary organization owned by the parent token holders.

So, we utilized ERC20Snapshot.sol to record parent organization balances, mint a subsidiary token allocation for the parent allocation, and then split this allocation based on the recorded parent allocations.

/// @notice Calculate a users cToken allocation
/// @param claimer Address which is being claimed for
/// @return cTokenAllocation Users cToken allocation
function calculateClaimAmount(address claimer)
  public
  view
  returns (uint256 cTokenAllocation)
  {
    cTokenAllocation = isSnapClaimed[claimer] ? 0 :
    (VotesToken(pToken).balanceOfAt(claimer, snapId) * pAllocation)
    /
    VotesToken(pToken).totalSupplyAt(snapId);
  } 

Thanks for reading!

Subscribe to Decent DAO
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.