Forwarder contract for Ether and ERC-20 tokens

Generating address contracts for each user to transfer their Ether and ERC-20 tokens to one main wallet.

Primoz Kocevar
VALUS
5 min readNov 21, 2018

--

The final solution is presented in this GitHub repository:

Reformatting a different solution

For this task code from this git repository was used and reformated:

However already when just trying to call a createForwarder() function of a deployed WalletSimple contract there was a problem. A call to the contract was made by signing it locally like so:

walletContract = web3.eth.contract(address="0xE81ab8f77a3Bbf2d19088D1d941270A2E3160825", abi=WalletSimple.ABI)

nonce = web3.eth.getTransactionCount(mainAccount.address)

tx = walletContract.buildTransaction({
'from': mainAccount.address,
'gasPrice': web3.toWei('14', 'gwei'),
'gas': 900000,
'nonce': nonce}).createForwarder()
signed = web3.eth.account.signTransaction(tx)
transaction_address = web3.eth.sendRawTransaction(signed.rawTransaction)
txReceipt = web3.eth.waitForTransactionReceipt(transaction_address)

However after the transaction was processed the txReceipt did not contain an address of a produced forwarder, which is a crucial piece of information for sending funds. Although this was required in the solidity constructor of the WalletSimple contract like:

constructor() public returns (address) {
return new Forwarder();
}

The reason this function did not return an address as specified is that this address is probably yet not defined the moment a constructor is called and thus can not be returned as the real network needs some time for this to process (this is probably why it worked testing on a local network).

Let us fix it

Thus a simple and quick fix was applied that admittedly consumes some additional gas. Adding two additional global variables was implemented.

A global variable is defined as soon as the constructor of Forwarder.sol is called so it is aware of its own address.

forwarderAddress = address(this);

This one is the forwarderAddress in the Forwarder.sol contract itself and it is necessary so that it could be called from the WalletSimple contract to set it as a second global variable latestForwarder like so:

Forwarder f = new Forwarder();
latestFowarder = f.forwarderAddress();

Needless to say, these two global variables automatically get getters defined in Solidity so they can be called and their value is publicly accessible on the Ethereum main net.

Adding an additional line to the web3.py part when calling WalletSimple is crucial:

walletContract = web3.eth.contract(address="0xE81ab8f77a3Bbf2d19088D1d941270A2E3160825", abi=WalletSimple.ABI)

nonce = web3.eth.getTransactionCount(mainAccount.address)

tx = walletContract.buildTransaction({
'from': mainAccount.address,
'gasPrice': web3.toWei('14', 'gwei'),
'gas': 900000,
'nonce': nonce}).createForwarder()
signed = web3.eth.account.signTransaction(tx)
transaction_address = web3.eth.sendRawTransaction(signed.rawTransaction)
forwarderAddress = walletContract.functions.latestFowarder().call()
txReceipt = web3.eth.waitForTransactionReceipt(transaction_address)

Explanation: we could not get the address through the constructor as on the real network an address of the deployed contract is not yet available at the same moment as constructor is called but a bit later, so that we have to get it via a global public variable of walletSimple contract named latestForwarder (a getter for this variable is simply called with () as any other function!

EFFICIENCY?

This approach is maybe not optimal as it consumes extra gas and the latestForwarder variable can be sensitive to multiple addresses calling it at once and could bring some unpredicted behaviour. Thus an approach mentioned in the documentation is also to be considered to more thoroughly refurbish mentioned contracts to return a correct address after the constructor:

However, this approach did not work for us, thus the first recommended approach was still a good call we decided as we call fowarderAddress getter immediately after receiving a receipt so it should always get the right address as we also do it in sequence one after the other and thus it should always work!

IMPROVEMENTS

However, the first approach could still be improved by using an array of addresses for saving all the forwarder addresses in the contract but this would probably be too expensive with gas. However, we could always read all of the created forwarders and export all the unused ones! An even better would be to send the new forwarder address using events and catch in on the server, however according to previous experiences with EVENTS, which are horribly unpredictable, especially using INFURA, this would be recommended at a later stage, and all the created forwarders could change their parent addresses using the newly implemented function changeParent().

On data formatting

It is crucial that inputting data is done using bytes32 format. Oddly enough the format must be in HEX and not in bytes (as the name would suggest) :

Therefore before inputting a string to a bytes32 variable in solidity contract, a conversion like this must be done:

bytesVariable = Web3.toHex(string)

Why use bytes over strings:

FINAL SOLUTION

For the final solution, events were used as a means of transferring created forwarder address to the caller of this action. It was somehow a lucky strike as it was obvious that a receipt of a createForwarder() includes some logs that include event logs.

an example of createForwarder() receipt in Remix that includes event logs in logs.

COMMENTS:

Here it must be said that in Remix if we only import WalletSimple contract as address without ABI the logs are empty when calling createForwarder(). This means that it is crucial to supply the correct ABI in web3.py so that we know events were called when calling createForwarder() and can thus get a return from it!

Calling createForwarder() without returned logs as it was imported as an address. (probably no ABI supplied)

The event about generating forwarders was emitted in solidity in a contract like:

function createForwarder() public payable returns (address) {
Forwarder f = new Forwarder();
emit ForwarderCreated(address(f));
}

Reading this using python:

txReceipt = web3.eth.waitForTransactionReceipt(transaction_address, timeout=3000)
# get forwarder address from event logs!
data = txReceipt["logs"][0]["data"]
fAddr = "0x" + data[26:66]
forwarderAddress = to_checksum_address(fAddr)

Additionally added an event for flushing tokens for future work so that we can follow flushing through events!

In the end, the whole contract was rewritten using the latest recommendations like in the solidity documentation.

--

--