How to set ownership for UUPS Factory deployed ERC20 tokens?

My project uses the UUPS upgradeable proxy mode, and the warpped ERC20 contract will be deployed through the Factory mode in the implementation contract.

There will be 3 contracts: proxy.sol, imple.sol and wrapERC20.sol.

In imple.sol, wrapERC20.sol will be deployed through the factory mode. At the same time, imple.sol needs to call wrapERC20.sol to mint or burn tokens.

The mint and burn functions require permissions, but I don’t know who to set the permissions to.

I want only imple.sol to mint and burn tokens, but imple.sol will be discarded after the upgrade.

:1234: Code to reproduce

Imple.sol :

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import {WrapERC20} from "./WrapERC20.sol";

contract Imple is UUPSUpgradeable, Ownable2StepUpgradeable {
    address public wToken;

    function initialize() public initializer {
        __UUPSUpgradeable_init();
        __Ownable_init(msg.sender);
    }

    function createToken(string memory name, string memory symbol) public onlyowner {
        WrapERC20 newToken = new WrapERC20(address(this), name, symbol);

        wToken = address(newToken);
    }

    function mintWToken(address _to, address _amount) public onlyOwner {
        IWrapERC20(wToken).mint(_to, _amount);
    }

    function burnWToken(address _from, address _amount) public onlyOwner {
        IWrapERC20(wToken).burn(_from, _amount);
    }
}

WrapERC20.sol:

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

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract WrapERC20 is ERC20 {
    address public factory;    // How to set?

    error NotFactory();

    constructor(address _factory, string memory _name, string memory _symbol) ERC20(_name, _symbol) {
        factory = _factory;
    }

    function mint(address _to, uint256 _amount) public {
        if(msg.sender != factory) revert NotFactory();
        _mint(_to, _amount);
    }

    function burn(address _from, uint256 _amount) public {
        if(msg.sender != factory) revert NotFactory();
        _burn(_from, _amount);
    }
}

Thanks for any suggestions!

If possible, please share the implementation contract.

Hey, the content has been updated. :folded_hands:

It seems like your contracts are right, Proxy is the factory in the WrapERC20 contract

Hmm... In my code, I set the factory to Imple.sol :
WrapERC20 newToken = new WrapERC20(address(this), name, symbol);

After the upgrade, imple.sol will be replaced, which makes it impossible to check permissions in wrapERC20. This is the problem I encountered.

You can write a test case to print the variable address(this), I think it is the proxy address. That is the delegatecall.

1 Like

Thanks for the correction, you are right. address(this) is the address of proxy.

But the problem seems to still exist. When wrapERC20 is called in imple.sol, msg.sender in wrapERC20 is the address of imple.sol. How should I check it?

I think I understand.

When wrapERC20 is called via imple.sol, msg.sender in wrapERC20 is the proxy contract address. Is that right?

The relationship between proxy and implementation is built on the delegatecall opcode, which allows the proxy contract to execute code from the implementation contract while preserving its own storage context.

So just like above, users call proxy, proxy delegatecalls the implementation, users are not expected to call the implementation.

1 Like