ERC20 Wrapper tutorial

In this tutorial we are going learn how to create a wrapped token using the ERC20Wrapper contract provided by OpenZeppelin, but first let's get familiar with what a wrapped token is.

Introduction to wrapped tokens

A wrapped token is a token whose value is tied to an underlying cryptocurrency. An amount of the original token is locked in a digital vault, and in return this allows an equivalent amount of wrapped tokens to be minted.

This is useful for several reasons, to mention a few:

  • To extend the functionalities of an existing token in conjunction with other ERC20 modules.
  • Allow a native cryptocurrency to behave like an ERC20, e.g. Wrapped ether (WETH).
  • Allow the use of currencies outside its native blockchain, e.g. Wrapped bitcoin (WBTC).

This OpenZeppelin contract covers the first case: underlying ERC20 tokens on the same chain can be wrapped and thus virtually extended with new functionality. For this tutorial we are implementing a wrapped token that enables voting in a governance protocol with a token that was not originally designed for this purpose.

Let’s start!
We’ll assume a project X was launched with its own token, a non upgradable ERC20 contract, but some time after the launch they found themselves in need of on chain governance. For said purpose they would need to deploy a governance system through a Governor contract that will look something like this:

contract MyGovernor is Governor, GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl {
    constructor(ERC20Votes _token, TimelockController _timelock)
        Governor("MyGovernor")
        GovernorSettings(1 /* 1 block */, 45818 /* 1 week */, 0)
        GovernorVotes(_token)
        GovernorVotesQuorumFraction(4)
        GovernorTimelockControl(_timelock)
    {}
 
    // ...
}

For more information on how to set an on chain governance please take a look at this guide.

As you might have noticed it uses an ERC20Votes type of token to perform all its operations, and the launched token was just a non upgradeable ERC20, therefore not compatible with the governance needs. The solution to their predicament is fairly simple, let's wrap the existing token adding the ERC20Votes module.

First we will inherit from the contracts ERC20Wrapper and ERC20Votes:

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Wrapper.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
 
contract VotesWrapper is ERC20, ERC20Wrapper, ERC20Votes {

ERC20Wrapper takes a parameter for the original token that we want to wrap. We would use the previously launched token address as this parameter. And we’ll setvalues for all the other required parameters, notice that we have to include ERC20Permit constructor call because it is a parent for our wrapped token parent ERC20Votes:

constructor(IERC20 wrappedToken)
   ERC20("Wrapped Token", "wTKN")
   ERC20Permit("Wrapped Token")
   ERC20Wrapper(wrappedToken)
{}

Note that the compiler requires writing some overrides.

Click here to see the full code


import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Wrapper.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
 
contract VotesWrapper is ERC20, ERC20Wrapper, ERC20Votes{
    constructor(IERC20 wrappedToken)
       ERC20("Wrapped Token", "wTKN")
       ERC20Permit("Wrapped Token")
       ERC20Wrapper(wrappedToken)
    {}
    
    function _afterTokenTransfer(address from, address to, uint256 amount)
        internal
        override(ERC20, ERC20Votes)
    {
        super._afterTokenTransfer(from, to, amount);
    }
 
    function _mint(address to, uint256 amount)
        internal
        override(ERC20, ERC20Votes)
    {
        super._mint(to, amount);
    }
 
    function _burn(address account, uint256 amount)
        internal
        override(ERC20, ERC20Votes)
    {
        super._burn(account, amount);
    }

Now you can use your wrapped token to execute any voting operations, by deploying the wrapped token first, and then deploying the Governor contract configured with the wrapped token as its voting token.

In order to vote for governance proposals, token holders must first use the wrapped token’s method depositFor to get voting tokens 1-to-1 for the tokens they already hold.

After there are some locked tokens on the wrapped token’s contract, or in other words after there are some wrapped tokens minted, you can start using the Governor contract as if you were using your token as a voting token.

8 Likes