Search code examples
ethereumsoliditysmartcontracts

create upgradable proxy contract that works with MetaMask


I'm creating a proxy contract that connect to existing ERC-20 contract. this proxy contract should able to connects with metamask and show token balances.

every things works fine when add token in metamask with proxy address, it show symbol and decimal number correctly but not balance. shown zero instead.

proxy contract code:

contract Proxy  {

    address private _implementation; 
    event Upgraded(address indexed implementation); 

    function implementation() public view returns (address) {
    return _implementation;
    }

    function upgradeTo(address impl) public  {
    _implementation = impl;
    emit Upgraded(impl);
    }


    function () payable external {
        address _impl = implementation();
        require(_impl != address(0));
        assembly {
            let ptr := mload(0x40)
            calldatacopy(ptr, 0, calldatasize) 
            let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
            let size := returndatasize
            returndatacopy(ptr, 0, size)

            switch result
            case 0 { revert(ptr, size) }
            default { return(ptr, size) }
       }
   }
}

the function balanceOf working fine when i add token in metamask with ERC-20 contract address. but show zero by proxy contract

function balanceOf(address tokenOwner) public view  returns (uint256) { 
    return balances[tokenOwner];
}

My efforts

for test i wrote this function:

function test(address theAddress) public view  returns (address) { 
    return theAddress ;
}

when i call argument '0xC357c241b98B15B3A08aeC3AcD49fBC0cbD74fcE' on ERC-20 contract returns same address but on proxy returns this value:

0xc357c241b98b19150f7f8f1d47ad1cd500000000

another test that i do is this function:

function test2(string memory theString) public view  returns (string memory) { 
    return theString ;
}

this function works fine on both proxy and ERC-20 contract!!

thanks all.

Edit 1

my test with web3.js

var interval ;
document.addEventListener('DOMContentLoaded', function() {
      interval =  setInterval(run , 1000);
}, false);


function run(){
    web3 = new Web3(web3.currentProvider); 
    console.log("call");
    if(web3.eth.accounts[0] === undefined)
        return;


    clearInterval(interval);
    console.log(web3.eth.accounts[0]); 

   web3.eth.defaultAccount = web3.eth.accounts[0];
  var CoursetroContract = web3.eth.contract( JSON.parse(`[

{
    "constant": true,
    "inputs": [
        {
            "name": "theAddress",
            "type": "address"
        }
    ],
    "name": "test",
    "outputs": [
        {
            "name": "",
            "type": "address"
        }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
},
{
    "constant": true,
    "inputs": [
        {
            "name": "theString",
            "type": "string"
        }
    ],
    "name": "test2",
    "outputs": [
        {
            "name": "",
            "type": "string"
        }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  }
]`));

 var contract = CoursetroContract.at('0xd3744cac3a2f796c16b45e5be582c1c5f3039482'); //proxy

//var contract = CoursetroContract.at('0xd025c8835b2a4bd2f9eeb1d682db224f7b301868'); //erc20

contract.test(0xC357c241b98B15B3A08aeC3AcD49fBC0cbD74fcE,
            function(err,result){
                console.log("err" ,err);
                console.log("result" , result);
            }
        );       

Edit 2

this contract addresses is already available in Ropsten Testnet


Solution

  • The proxy contract works a bit differently.

    let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
    

    The DELEGATECALL in your proxy contract calls the contract via the address specified in _impl. So, as a result, it runs the _impl code ( in your case ERC20 ) in proxy contracts's environment. As a result, the storage of proxy is modified and not ERC20 contracts storage. Link to how delegatecall works.

    So my suggestion would be to look at how you are initializing your ERC20 contract and setting its balance.

    You would have to do something like this

    erc20Contract = await erc20Contract.at(proxy.address)
    
    erc20Contract.initialize()
    

    The first line gives you the interface of erc20Contract at proxy contract's address. And the second line would redo the work of the constructor at proxy contract's address and storage.