Hey everyone! So I’m trying to create a Forwarder
contract that converts all received ETH/tokens to Chai and sends the Chai to a user’s address. The address that Chai gets forwarded to is the owner
of the Forwarder
contract, so each user should have their own Forwarder
contract. As a result, I figured I’d deploy each contract as a minimal proxy to reduce gas costs. I’d also like to be able to upgrade the underlying logic of the Forwarder contract, and, since the proxy contracts delegate calls to this contract, my understanding is all proxy contracts would consequently be updated by simply updating the logic contract.
I also want to keep track of all deployed contracts on chain, so am using a factory contract to do this. The problem I’m having is that when the factory deploys the proxy in the createForwarder()
function, I can’t seem to call the initialize()
function of the Forwarder
proxy contract afterwards in the same transaction. (It works as a separate transaction sent from JS, but this opens up the possibility of front-running).
There’s two approaches I’ve tried, and they both fail differently. The ForwarderFactory
code below has comments explaining exactly how each approach fails. I’m running the tests with Truffle (v5.1.3) and ganache-cli (ganache-cli v6.6.0, ganache-core v2.7.0) on a forked mainnet.
Any help is appreciated!
The truffle test is as follows:
const Factory = artifacts.require('ForwarderFactory');
const Forwarder = artifacts.require('Forwarder');
beforeEach(async () => {
// Deploy Forwarder logic template
// Here, admin refers to our server
ForwarderInstance = await Forwarder.new({ from: admin });
forwarder = ForwarderInstance.address;
// ***QUESTION***: should the proxy's logic template be initialized as done here?
// Whether initialized or not, I have the same problem
await ForwarderInstance.methods['initialize(address,address)'](admin, admin, { from: admin });
// Deploy factory
FactoryInstance = await Factory.new({ from: admin });
factory = FactoryInstance.address;
await FactoryInstance.methods['initialize()']({ from: admin });
});
it('deploys a clone and updates the state', async () => {
// Create forwarder (alice is a user)
await FactoryInstance.createForwarder(forwarder, alice, { from: admin, gas: '6000000' });
// Confirm state was updated and that factory is a clone
const forwarders = await FactoryInstance.getForwarders(); // get array of all forwarders
const users = await FactoryInstance.getUsers(); // get array of all users
expect(forwarders).to.be.an('array').with.lengthOf(1); // passes
expect(users).to.be.an('array').with.lengthOf(1); // passes
expect(users[0]).to.equal(alice); // passes
const isClone = await FactoryInstance.isClone(forwarder, forwarders[0], { from: admin });
expect(isClone).to.be.true; // passes
// Create instance of deployed factory proxy and check the owner and version
const ForwarderProxy = await new web3.eth.Contract(forwarderAbi, forwarders[0]);
const owner = await ForwarderProxy.methods.owner().call();
const version = await ForwarderProxy.methods.version().call();
expect(owner).to.equal(users[0]); // fails, owner is still the zero address
expect(version).to.equal('1'); // fails, version is still zero
// The below commented out line does successfully initialize the contract,
// but opens us up to front-running since it's a separate transaction
// await ForwarderProxy.methods.initialize(alice, admin).send({ from: admin });
});
Here are the relevant parts of the Forwarder contract:
import "@openzeppelin/contracts-ethereum-package/contracts/ownership/Ownable.sol";
import "@openzeppelin/upgrades/contracts/Initializable.sol";
contract Forwarder is Initializable, Ownable {
address public admin;
uint256 public version;
event Initialized(address indexed thisAddress);
function initialize(address _recipient, address _admin) public initializer {
emit Initialized(address(this));
Ownable.initialize(_recipient);
admin = _admin;
version = 1;
// other functions omitted for brevity...
// there is a fallback function in case that helps
}
And here are the relevant parts of the Factory contract:
import "@openzeppelin/contracts-ethereum-package/contracts/ownership/Ownable.sol";
import "@openzeppelin/upgrades/contracts/Initializable.sol";
import "./Forwarder.sol";
contract ForwarderFactory is Initializable, Ownable {
address[] public forwarders;
address[] public users;
mapping (address => address) public getForwarder; // maps user => forwarder
event ForwarderCreated(address indexed _address);
function initialize() public initializer {
Ownable.initialize(msg.sender);
}
function createForwarder(address _target, address _user) external {
// Deploy proxy
address _forwarder = createClone(_target);
emit ForwarderCreated(_forwarder);
// Update state
forwarders.push(_forwarder);
users.push(_user);
getForwarder[_user] = _forwarder;
// Initialize the new contract
// There are two approaches shown below which both do not work. Here
// they are both not commented out so it's easier to read, but of course
// when testing only one was used at a time
address _admin = owner();
// APPROACH 1
// - The transaction completes, but tests show the new contract wasn't
// actually initialized, as the owner is the zero address and the version
// is still set to the default of zero.
// - The factory's `ForwarderCreated` event is emitted, but the forwarder's
// `Initialized` event is not emitted, which makes me think I'm calling the
// initialization function wrong
bytes memory _payload = abi.encodeWithSignature("initialize(address,address)", _user, _admin);
(bool success,) = _forwarder.call(_payload);
require(success, "forwarderFactory/forwarder-proxy-initialization-failed");
// APPROACH 2
// - This fails with "VM Exception while processing transaction: revert"
// - If using `_forwarder` instead of `_forwarderPayable` to call the initialize
// function, compilation fails with "TypeError: Explicit type conversion not
// allowed from non-payable "address" to "contract Forwarder", which has
// a payable fallback function."
address payable _forwarderPayable = address(bytes20(_forwarder));
Forwarder(_forwarderPayable).initialize(_user, _admin);
}
function createClone(address target) internal returns (address result) {
// source: https://github.com/optionality/clone-factory/blob/master/contracts/CloneFactory.sol
bytes20 targetBytes = bytes20(target);
assembly {
let clone := mload(0x40)
mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
mstore(add(clone, 0x14), targetBytes)
mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
result := create(0, clone, 0x37)
}
}
// other functions omitted for brevity...
// no fallback function here