0

We are launching an ERC20Upgradeable token using the standard proxy pattern and OpenZeppelin templates.

Further, we are using Gnosis safe for multisig functionality.

A UX annoyance of Gnosis safe is that the token balance does not appear in the asset list of the safe. This means that users must click on 'New Transaction' > 'Contract interaction' and read their balance from the chain.

I believe that the token balance does not appear in the asset list because:

  • the proxy contract is not ERC-20 (after all, it is a proxy, not a token contract), and
  • the logic contract does not store state (thus, it does not capture the fact that the safe holds a nonzero quantity of tokens).

Is there any way that we can get the Gnosis UI to track upgradeable token balances and present them directly to users?

EDIT: Below are two contracts. They are identical except that one is upgradeable and the other isn't. Only the non-upgradeable one appears as an asset in Gnosis safe.

Upgradeable contract:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";

contract PureToken is Initializable, ERC20Upgradeable, AccessControlUpgradeable {

    bytes32 public constant USER_ROLE = keccak256("USER_ROLE");

    function initialize() public initializer {
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        __ERC20_init("PureToken","PPT");
        _mint(address(0xeF008d5228cEFD52d806876586aFf7d0371Eb91B), 17 * 10 ** decimals());
    }
    
    function mint(address to, uint256 amount) public {
        require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Must be ADMIN.");
        require(hasRole(USER_ROLE, to), "Recipient must be USER.");
        _mint(to, amount);
    }

    function burn(address from, uint256 amount) public {
        require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Must be ADMIN.");
        _burn(from, amount);
    }

    function transfer(address to, uint256 value) public override returns (bool) {
        require(hasRole(USER_ROLE, to), "Recipient must be USER.");
        return super.transfer(to, value);
    }

    function transferFrom(address from, address to, uint256 value) public override returns (bool) {
        require(hasRole(USER_ROLE, to), "Recipient must be USER.");
        require(hasRole(USER_ROLE, msg.sender), "You must hold USER status to send tokens.");
        return super.transferFrom(from, to, value);
    }
}

Non-upgradeable token

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

contract PureToken is Initializable, ERC20, AccessControl {

    bytes32 public constant USER_ROLE = keccak256("USER_ROLE");
    
    constructor() ERC20("PureToken", "PPT") {
        _mint(address(0xeF008d5228cEFD52d806876586aFf7d0371Eb91B), 19 * 10 ** decimals());
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

     function mint(address to, uint256 amount) public {
        require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Can not mint: you are not an ADMIN.");
        require(hasRole(USER_ROLE, to), "Can not mint: recipient is not a USER.");
        _mint(to, amount);
    }

    function burn(address from, uint256 amount) public {
        require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Can not burn: you are not an ADMIN.");
        _burn(from, amount);
    }

    function transfer(address to, uint256 value) public override returns (bool) {
        require(hasRole(USER_ROLE, to), "Can not transfer: recipient is not a USER.");
        return super.transfer(to, value);
    }

    function transferFrom(address from, address to, uint256 value) public override returns (bool) {
        require(hasRole(USER_ROLE, to), "Can not transfer: recipient is not a USER.");
        require(hasRole(USER_ROLE, msg.sender), "You must hold USER status to send tokens.");
        return super.transferFrom(from, to, value);
    }
}
3
  • help.gnosis-safe.io/en/articles/…
    – mikheevm
    Jun 17, 2022 at 8:44
  • This article is about how to get an icon in the UI. I am interested in getting the token balance in the UI.
    – N. Mao
    Jun 17, 2022 at 8:59
  • Does your token implement transfer events correctly?
    – mikheevm
    Jun 17, 2022 at 9:17

3 Answers 3

2

Gnosis Safe works with any ERC-20 token, including proxy tokens.

There must be some other problem that is not obvious from your question as it lacks

  • Method how you test with your token with Gnosis Safe

  • Your token source code

5
  • I deploy the contract to Rinkeby, mint tokens to the safe address, and then see whether the token appears in the asset list. The token does not appear in the list, but I can interact with the token proxy as expected using 'New Transaction' > 'Contract interaction'. I have experimented with launching another token with exactly the same logic, other than being non-upgradeable. This token does appear in Gnosis, leading me to believe that proxy tokens weren't supported by Gnosis. I'll post the source code in the original question.
    – N. Mao
    Jun 17, 2022 at 9:14
  • Did you add your token to Gnosis safe via Manage tokens list? Jun 17, 2022 at 9:45
  • Mikko, I do not see that button anywhere. It certainly does not appear anywhere on the page that lists assets. (I am using the interface in the browser.)
    – N. Mao
    Jun 17, 2022 at 10:05
  • Can you contact Gnosis Safe Discord and ask them there? Jun 17, 2022 at 10:11
  • I can, but I suspect something else is wrong because the non-upgradeable tokens appear automatically.
    – N. Mao
    Jun 17, 2022 at 10:20
1

I suspect you are right that the reason is that your proxy contract is not an ERC20 contract. Since you are using OpenZeppelin upradeability, your proxy contract doesn't expose the methods of an ERC20 contract, such as decimals, balanceOf, transfer, but rather it exposes methods like implementation, upgradeTo, etc. Therefore gnosis has no easy way of detecting that it is an ERC20.

0

Credit to the Gnosis team on Discord for this answer to my question:

It turns out that a token with an empty name and symbol will not be displayed in the asset list on Gnosis.

The contract given in the question does define a name and symbol, but due to an unrelated error, a previous version of the contract - one with no name or symbol - had been deployed. When deploying this contract, it appears in Gnosis as desired.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.