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);
}
}