Skip to content

pcaversaccio/erc20-permit-upgradeable

Repository files navigation

Permit-Enabled, Upgradeable ERC20 Smart Contract Template

build status License: MIT

This repository provides two upgradeable ERC20 smart contract templates that include the new function permit, which allows users to modify the allowance mapping using a signed message (via secp256k1 signatures), instead of through msg.sender. Or in other words, the permit method, which can be used to change an account's ERC-20 allowance (see IERC20.allowance) by presenting a message signed by the account. By not relying on IERC20.approve, the token holder account doesn't need to send a transaction, and thus is not required to hold ether (ETH) at all.

The two templates are built on an abstract base contract:

  • Proxy: Abstract contract implementing the core delegation functionality.

In order to avoid clashes with the storage variables of the implementation contract behind a proxy, I use EIP1967 storage slots:

  • ERC1967Upgrade: Internal functions to get and set the storage slots defined in EIP1967.
  • ERC1967Proxy: A proxy using EIP1967 storage slots. This proxy contract is not upgradeable by default.

There are two alternative ways to add upgradeability to an ERC1967 proxy. Their differences are explained in Transparent vs UUPS Proxies:

Transparent vs UUPS Proxies

Disclaimer: This section is based on OpenZeppelin's documentation. I strongly recommend anyone who is starting to use upgradeable smart contracts to read OpenZeppelin's documentation carefully. You can find more links to get started in the References section below.

The original proxies included in OpenZeppelin followed the Transparent Proxy Pattern. While this pattern is still provided, OpenZeppelin's recommendation is now shifting towards UUPS proxies, which are both lightweight and versatile. The name UUPS comes from EIP1822, which first documented the pattern.

While both of these share the same interface for upgrades, in UUPS proxies the upgrade is handled by the implementation, and can eventually be removed. Transparent proxies, on the other hand, include the upgrade and admin logic in the proxy itself. This means TransparentUpgradeableProxy is more expensive to deploy than what is possible with UUPS proxies.

UUPS proxies are implemented using an ERC1967Proxy. Note that this proxy is not by itself upgradeable. It is the role of the implementation to include, alongside the contract's logic, all the code necessary to update the implementation's address that is stored at a specific slot in the proxy's storage space. This is where the UUPSUpgradeable contract comes in. Inheriting from it (and overriding the _authorizeUpgrade function with the relevant access control mechanism) will turn the contract into a UUPS compliant implementation.

Note that since both proxies use the same storage slot for the implementation address, using a UUPS compliant implementation with a TransparentUpgradeableProxy might allow non-admins to perform upgrade operations.

By default, the upgrade functionality included in UUPSUpgradeable contains a security mechanism that will prevent any upgrades to a non-UUPS-compliant implementation. This prevents upgrades to an implementation contract that wouldn't contain the necessary upgrade mechanism, as it would lock the upgradeability of the proxy forever. This security mechanism can be bypassed by either of:

  • Adding a flag mechanism in the implementation that will disable the upgrade function when triggered.
  • Upgrading to an implementation that features an upgrade mechanism without the additional security check, and then upgrading again to another implementation without the upgrade mechanism.

Transparent-Based, Permit-Enabled Smart Contract Template: ERC20PermitTransparentUpgradeable

The transparent-based, permit-enabled smart contract template contains the following features:

  • mintable: Privileged accounts will be able to create more supply. Note: The current template implementation mints 100 tokens at initialisation.
  • burnable: Token holders will be able to destroy their tokens.
  • pausable: Privileged accounts will be able to pause the functionality marked as whenNotPaused. Useful for emergency response.
  • permit: Without paying gas, token holders will be able to allow third parties to transfer from their account.
  • roles (for access control): Flexible mechanism with a separate role for each privileged action. A role can have many authorised accounts.
  • transparent (upgrade pattern): Uses more complex proxy with higher overhead, requires less changes in your contract.

Caveat: In the context of upgradeable contracts, implementation contracts should move the code within the constructor to a regular initializer function and have this function called whenever the proxy links to this logic contract. The main initialize() function of the template also contains the name and symbol of the ERC20 token contract, as well as the string name used for the permit function. Make sure you adjust these parameters accordingly.

UUPS-Based, Permit-Enabled Smart Contract Template: UUPSUpgradeable

The transparent-based, permit-enabled smart contract template contains the following features:

  • mintable: Privileged accounts will be able to create more supply. Note: The current template implementation mints 100 tokens at initialisation.
  • burnable: Token holders will be able to destroy their tokens.
  • pausable: Privileged accounts will be able to pause the functionality marked as whenNotPaused. Useful for emergency response.
  • permit: Without paying gas, token holders will be able to allow third parties to transfer from their account.
  • roles (for access control): Flexible mechanism with a separate role for each privileged action. A role can have many authorised accounts.
  • UUPS (upgrade pattern): Uses simpler proxy with less overhead, requires including extra code in your contract. Allows flexibility for authorising upgrades.

Caveat: In the context of upgradeable contracts, implementation contracts should move the code within the constructor to a regular initializer function and have this function called whenever the proxy links to this logic contract. The main initialize() function of the template also contains the name and symbol of the ERC20 token contract, as well as the string name used for the permit function. Make sure you adjust these parameters accordingly.

Upgrading an Upgradeable Contract

Let's say you have deployed one of the two template smart contracts and want to add another feature, e.g. flash minting (i.e. lending tokens without requiring collateral as long as they're returned in the same transaction). Since the proxy contract has already called initialize in the context of the first implementation contract, we will need to create a new function or modifier called upgradeToV2 that will allow the initialisation of the required parameters for the flash mint (or any newly added) feature. This could look as follows as a function (without taking into account all other contract-specific dependencies):

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract MyContract is Initializable {
  uint256 private _version;

  function initialize() public initializer {
    _version = 1;
  }

  function upgradeToV2() public onlyRole(UPGRADER_ROLE) {
    require(_version < 2, "MyContract: Already upgraded to version 2");
    _version = 2;
  }
}

Modifying Your Contracts

When writing new versions of your contracts, either due to new features or bug fixing, there is an additional restriction to observe:

  • you cannot change the order in which the contract state variables are declared,
  • nor their type!

You can read more about the reasons behind this restriction in Proxies.

Further Notes

Multiple Inheritance

Initialiser functions are not linearised by the compiler like constructors. Because of this, each __{ContractName}_init function embeds the linearised calls to all parent initialisers. As a consequence, calling two of these init functions can potentially initialise the same contract twice.

The function __{ContractName}_init_unchained found in every contract is the initialiser function minus the calls to parent initialisers, and can be used to avoid the double initialisation problem, but doing this manually is not recommended.

Storage Gaps

You may notice that every OpenZeppelin contract includes a state variable named __gap. This is empty reserved space in storage that is put in place in upgradeable contracts. It allows us to freely add new state variables in the future without compromising the storage compatibility with existing deployments.

It isn't safe to simply add a state variable because it shifts down all of the state variables below in the inheritance chain. This makes the storage layouts incompatible, as explained in Writing Upgradeable Contracts. The size of the __gap array is calculated so that the amount of storage used by a contract always adds up to the same number (in this case 50 storage slots).

Deployments

I make use of OpenZeppelin's upgrade plugin for Hardhat. You can find a sample deployment and upgrade script in the file deploy.ts. It is strongly recommended to test upgrades first on test networks!

Unit Tests

Since Hardhat implements great features for Solidity debugging like Solidity stack traces, console.log, and explicit error messages when transactions fail, we leverage Hardhat for testing:

npm run test

The unit tests are based on OpenZeppelin's available unit tests.

Ethereum Test Network Deployments

The smart contracts ERC20PermitTransparentUpgradeable.sol and ERC20PermitUUPSUpgradeable.sol have been deployed across all the major Ethereum test networks:

TransparentUpgradeableProxy Deployments

*Built-in flash loans added.

UUPSUpgradeable Deployments

*Built-in flash loans added.

References

[1] https://eips.ethereum.org/EIPS/eip-1822

[2] https://blog.openzeppelin.com/the-transparent-proxy-pattern

[3] https://docs.openzeppelin.com/contracts/4.x/upgradeable

[4] https://docs.openzeppelin.com/openzeppelin/upgrades

[5] https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable