2

I'm playing with the upgradeable contracts via DelegateProxy in Solidity.

This pattern posits that a MutableForwarder contract is deployed once, and all the subsequent calls to the upgradeable contract are made to the fallback function of the Forwarder, which in turn uses delegatecall to invoke the target contract.

I have a hard time calling the fallback function:

var payload = testContract.contract.construct.getData(1);

return web3.eth.sendTransaction({
  from: accounts[0],
  to: testContractForwarder.address,
  data: payload
});

The results of running the deployment script:


Replacing TestContract...
  TestContract: 0x93d8c7d954a930f2eadf6fc68058e5c43ebe988f
Deploying TestContractForwarder...
  TestContractForwarder: 0xd46539cae056345ce547e65c3e8d8ec764ed5d1d
@@@ TestContractForwarder target: 0x93d8c7d954a930f2eadf6fc68058e5c43ebe988f

After linking the target to the forwarder, next transaction goes through, but has no effect:


@@@ TestContract/construct tx 0x11bea1b01e4977747bfd8b604eaf9fc3ebc5dae2b42bde7e918f811fbad1fd53 successful
--- TestContract/value : 0
--- TestContract/wasConstructed : false

Calling the contract directly, with the same payload is successful and sets the value.

Repository which reproduces: https://github.com/fbielejec/truffle-forwarder

  • Can you make a Github repo with your contract code and the migration scripts? – Xavier Leprêtre B9lab Nov 16 '18 at 12:03
  • sure, see the edited question. – fbielejec Nov 16 '18 at 12:05
  • In your migration script, the first thought I had was "why don't you .then all the actions?" From the look of it, there are a large number of independent promise chains, and it is unclear in which order they will complete. Can you .then all in a very strict fashion? – Xavier Leprêtre B9lab Nov 16 '18 at 12:12
  • Second thought is "why do you linkBytecode with a fake beefbeefbeefbeefbeefbeefbeefbeefbeefbeef?". – Xavier Leprêtre B9lab Nov 16 '18 at 12:14
  • a) I don't think this is neccessary, or even part of the problem, the steps are queued truffleframework.com/docs/truffle/getting-started/… and the logged output confirms that. b) to replace the target contract address in the bytecode. – fbielejec Nov 16 '18 at 12:16
2

You misunderstand what a forwarder does when it uses delegatecall. What delegatecall does is use the code of elsewhere to run in the context of the contract that used delegatecall.

So if your instance of TestContractForwarder "delegate calls" the code of TestContract, then you can expect the storage of your instance of TestContractForwarder to be updated.

Let's review your migration script. You write:


var payload = testContract.contract.construct.getData(1);
return web3.eth.sendTransaction({
    from: address,
    to: testContractForwarder.address,
    data: payload,
    gas: gas
});

Which you can change to:


return TestContract.at(testContractForwarder.address)
    .construct(1, { from: address, gas: gas })

Then when you want to confirm the change, you need to change your:


    return TestContract.deployed ();
}).then ((instance) => {
    return [instance.value (), instance.wasConstructed ()];
}).then ((promises) => {
    return Promise.all (promises);

Into:


    return TestContractForwarder.deployed (); // <-- Notice the difference
}).then ((instance) => {
    var myContract = TestContract.at(instance.address);
    return Promise.all([myContract.value (), myContract.wasConstructed ()]);

So now you have a problem. You see, MutableForwarder stores target at storage slot 0, and TestContract stores value at storage 0 too. So when you call .construct, in effect, you overwrite the target field.

To fix it, you need to have TestContract have a dummy field at storage slot 0. Like so:


contract TestContract {

  uint private dummyTargetDontUpdate; // Storage slo 0
  uint public value; // slot 1
  bool public wasConstructed; // slot 2

I rewrote your migration script:


  deployer.deploy (TestContract, opts)
    .then (instance => {
      linkBytecode(TestContractForwarder, forwarderTargetPlaceholder, instance.address);
      return deployer.deploy(TestContractForwarder, opts);
    })
    .then (instance => instance.target())
    .then (res => {
      console.log ("@@@ TestContractForwarder target:",  res);
      return Promise.all([ TestContract.deployed(), TestContractForwarder.deployed() ]);
    })
    .then (([testContract, testContractForwarder]) => TestContract.at(testContractForwarder.address)
        .construct(1, { from: address, gas: gas }))
    .then (tx => {
      console.log ("@@@ TestContract/construct tx", tx.tx, "successful");
      return TestContractForwarder.deployed();
    })
    .then (instance => {
      var myContract = TestContract.at(instance.address);
      return Promise.all([ myContract.value(), myContract.wasConstructed() ]);
    })
    .then (([res1, res2]) => {
      console.log ("--- TestContract/value :", res1.c [0]);
      console.log ("--- TestContract/wasConstructed :", res2);
      console.log ("Done");
    });
  • 1
    That's a very well put together answer, thank you. – fbielejec Nov 16 '18 at 14:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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