In this article I outline a method for sending arbitrary amounts of arbitrary transactions in bulk within a single transaction on the Ethereum chain. I also demonstrate the flexibility of the method in practice by performing the following four transactions within one:
1. Send 1 Ether to some other wallet
2. Send 1 ERC223 token to some other wallet
3. Call a contract with no function arguments
4. Call a contract with some function arguments as well as an Ether value
For a TLDR version, you may skip directly to the Usage Demonstration section. For an in-depth version you may continue reading here and I shall guide you down the rabbit hole.
One of the main issues blockchains are facing today is their lack of speed. Transactions on the Ethereum Mainnet can take minutes to mine, which is unacceptable for any application aspiring to be a true on-chain DApp (Decentralized Application).
In an ideal world, we would have a blockchain that can mine any amount of transactions in sub-second times, would never get congested by traffic, and would scale well enough to cover all demand for space. In such an ideal world, any and every interaction with the DApp User Interface could be a transaction, much like today it may be an API call.
Unfortunately, we do not live in an ideal world. So how close can we get to this ideal scenario with the current state of the Ethereum Mainnet? I posed this question to myself a while ago and decided to investigate. Turns out, the answer is: Closer than you may think.
While we are not able to address the space issue, we are able to address the speed issue to some extent. This article is the story of writing the fastest Ethereum wallet, one which brings the transaction-per-mouse-click idea closer to the realm of possibility.
My goal was to write a smart contract, which would have a special function with a single purpose: To receive multiple transactions encoded into its payload and fire them all in rapid succession within a single transaction. (You may sense now where the name “Ethereum Gatling Gun” came from..) Such a contract would allow composing a user’s actions in the DApp UI into a sequence of on-chain operations, which could all be fired together once the user is done making changes.
This contract would also be fully extensible to operate all on-chain counterparts of any additional UI functionality that may be added in the future of a DApp’s life, since it acts as a wallet itself.
Before we continue, I must warn readers that the work and code presented in this article is experimental and potentially very dangerous if used incorrectly! Any contract capable of firing hundreds of transactions in rapid succession must be operated with extreme caution, for obvious reasons. Also, this article is half article, half technical rant.
We can start off simple and write a smart contract that can forward a transaction onto the network in its own name. The
address
type in Solidity has a call
function, which can be used to call one contract from another contract.Forwarding transactions using the
call
function is exactly how Multi-Signature Wallets (MultiSigs) achieve their purpose. Instead of forwarding incoming transactions instantly, they save the transaction into storage and only forward it once all the MultiSig owners approve it.We can use the
call
function to create a very simple Proxy contract, which forwards all transactions instantly:pragma solidity ^0.4.25;
contract Proxy {
function execute(
address target, uint256 weiValue, bytes payload
) public {
target.call.value(weiValue)(payload);
}
}
I have deployed this contract (along with some fallback functions, because why not) here for anybody to play with. It has no concept of ownership, so it will forward transactions from literally anybody.
To demonstrate the contract in action, I transferred a single Bether ERC223 token to the contract in this transaction. I then called the Proxy in another transaction, instructing the Proxy to send the token back, which it did.
Our next step is to extend this contract to fire multiple transactions, not just one.
The obvious way to extend the Proxy contract to forward many transactions together would be to turn the arguments of the
execute
function into arrays and simply loop over them in the function like this:pragma solidity ^0.4.25;
contract ArrayProxy {
function execute(
address[] targets, uint256[] weiValues, bytes[] payloads
) public {
for(uint256 i = 0; i < targets.length; i++){
targets[i].call.value(weiValues[i])(payloads[i]);
}
}
}
There is, however, an issue. The Solidity 0.4.25 compiler does not support function arguments that are arrays of variable-length types. Out
bytes[]
argument is exactly such: bytes
is a variable-length type and we want an array of them. The Solidity compiler will complain about this with the following message:Error: This type is only supported in the new experimental ABI encoder.
Use "pragma experimental ABIEncoderV2;" to enable the feature.
address[] targets, uint256[] weiValues, bytes[] payloads
^--------------^
The experimental ABI encoder mentioned in the above error message is being slowly extended in the newest releases of the Solidity Compiler 0.5.
There are several issues with this encoder. First of all, it is experimental, and activating anything labeled as “experimental” in the context of blockchains is a red flag in and of itself.
The delightfully ominous warning message you get when you do try to turn on the experimental encoder, “Do not use experimental features on live deployments”, does not help.
Furthermore, the encoder is a work in progress, meaning features are being added to it all the time, and I was not going to wait for and trailblaze experimental features with live mainnet rapid-fire wallets…
So how can we build a rapid-fire wallet in Solidity 0.4.25 without any experimental features? We must go beyond Solidity. We must venture into the dreaded EVM bytecode itself.
While this may be a subjective feeling, it seems to me that the line between Solidity and the EVM (Ethereum Virtual Machine) is extremely blurred in the Blockchain community, to the point where “smart contracts” are almost synonymous with “Solidity”. Lets clearly disambiguate them.
All smart contracts on the Ethereum network are written in bytecode (the hexadecimal string you see on Etherscan when you open up the code of a contract). The EVM is responsible for executing bytecode.
Solidity is a programming language that compiles human-readable code into bytecode. The reason we use Solidity is that writing entire contracts in bytecode by hand is extremely inconvenient, and Solidity is the most popular compiler that generates EVM bytecode. But, fundamentally, there is nothing preventing you from writing a smart contract in hexadecimal by hand without the help of Solidity.
This distinction is crucial, because smart contracts written in Solidity can only leverage as much of the EVM functionality as the current Solidity compiler supports. If you want your smart contract to do something Solidity doesn’t support, you can still implement it by writing the bytecode yourself.
Conveniently, Solidity gives programmers the option to specify bytecode in the
assembly
blocks in Solidity code, in case we ever needed to go beyond what Solidity can do. This is equivalent to telling Solidity: “You know what, don’t even bother trying to generate bytecode for this particular bit, just use this bytecode I wrote myself”.As it turns out, while the
bytes[]
type is not supported by solidity, we could emulate it (in some alternate form) ourselves using bytecode. So let us do just that.The first thing we need to realize is that we don’t necessarily need the explicit
bytes[]
type at all. What we actually need is to call another contract with some data, and the bytes[]
type is just the way Solidity would have done it (if it could). But Solidity != EVM, so let’s see how the EVM performs internal transactions and what form it needs the payloads in.The EVM has a
CALL
opcode, which triggers internal transactions. This is how we manually invoke it from Solidity:assembly{
call(
<gas_limit>,
<target>,
<wei_value>,
<data_location>,
<data_length>,
<output_location>,
<output_length>
)
}
If we want to use the
CALL
operation, we must provide/compute the seven arguments it consumes. The two arguments that concern us are the <data_location>
and <data_length>
, since they are the ones that define the payload we send. The rest are trivial.Basically, when we invoke the
CALL
operation, it will call the target contract with a payload comprised of the first <data_length>
bytes located at memory location <data_location>
. This means that if we can somehow get all the payloads of all the transactions into one contiguous area in memory, we can loop over limit/offset pairs and invoke CALL
directly on the computed memory locations, triggering all the transactions.Luckily, there is a very convenient data type in Solidity for storing contiguous bytes. It is, somewhat unsurprisingly,
bytes
. We can replace our desire for a bytes[]
type with a simple bytes
argument, and we can add a new uint256[]
argument of payload lengths for navigating the contiguous payloads in the single bytes
argument.So let’s stitch it all together. We shall create a fire function, which will have the following signature:
function fire(
bytes, address[] targets, uint256[] lengths, uint256[] values
)
The first
bytes
argument consists of all the payloads of all the transactions simply appended to each other. The targets
argument is an array of addresses of the contracts you wish to call. The lengths
array is an array of lengths of the individual payloads in the bytes
argument, and finally, the values
argument are the values in wei that are to be attached to each transaction.The first thing our function must do is ensure that all the arrays are the same length:
require(targets.length == lengths.length);
require(targets.length == values.length);
Next, we move the
bytes
argument into memory. This is a bit more involved, since we need to find out where in the calldata the bytes array is located, how long it is, move it to memory and shift the free memory pointer (0x40) to the end of the data. We do all this in assembly:uint256 payloadMemoryLocation;
assembly {
payloadMemoryLocation := mload(0x40)
let payloadLengthLocation := add(4, calldataload(4))
let payloadLength := calldataload(payloadLengthLocation)
let payloadLocation := add(32, payloadLengthLocation)
calldatacopy(payloadMemoryLocation, payloadLocation, payloadLength)
mstore(0x40, add(payloadMemoryLocation, payloadLength))
}
Once the payloads are in memory, we can set up the loop over all transactions, which computes the limits and offsets. We also require that all the internal transactions succeeded, to ensure either all transactions complete or none do. This is all done in Solidity:
uint256 offset = 0;
bool success;
for(uint256 i = 0; i < targets.length; i++){
address target = targets[i];
uint256 limit = lengths[i];
uint256 value = values[i];
# ASSEMBLY MAGIC HERE
require(success);
offset += limit;
}
Now all we need to do is replace the “ASSEMBLY MAGIC” part with actual assembly, that invokes the
CALL
operation on the correct payload:assembly {
success := call(
gas,
target,
value,
add(payloadMemoryLocation, offset),
limit,
0,
0
)
}
That’s it! Combined, this code allows you to execute hundreds of arbitrary transactions within a single transaction.
But enough chit-chat, let’s demonstrate it in practice.
First thing we must do is get the Gatling Gun wallet onto the blockchain. To make this process easier, I wrote and deployed a Gatling Gun Deployment contract here (equivalent contract is also here on Ropsten if you want to play with it using testnet Ether) This contract allows anybody to get themselves their own Gatling Gun wallet by simply calling the
deployGatlingGun(address owner)
function of this contract, which creates a new one and sets the owner address to the one provided.Actually operating the Gatling Gun wallet is still fairly complicated, so I put together a very simple minimalist UI, which allows deploying and operating these wallets with relative ease via Metamask.
The UI is at eth-gatling-gun.com, along with a demo video of how it can be used. This UI is only meant as a testing ground for operating the wallet. If you decide to use the Gatling Gun wallets in any project/DApp, I would recommend interfacing with it directly from your own client code via Metamask.
If you head over to the Operation tab, you will be presented with this screen:
Supposing you don’t have one yet, you may click the “Deploy New Gatling Gun” button, which will take you automatically to the following screen after the transaction is mined:
The address at the top will be the address of your new Gatling Gun wallet. The text field with the “Add Transaction(s)” button is where we put all the transactions we want to send. Remember, each transaction has a target address, an amount of Ether, and a Payload.
We need to specify these three values (separated by a minus sign) for each transaction. We can specify more of these triplets (multiple transactions) by separating them with the semicolon.
Now we are ready for executing the transactions mentioned in the article’s preface. For this demo I decided the following operations all fired at once should sufficiently showcase the wallet’s flexibility:
1. Send 1 Ether to some other wallet
2. Send 1 ERC223 token to some other wallet
3. Call a contract with no function arguments
4. Call a contract with some function arguments as well as an Ether value
Though, of course, you can fire a lot more than four if you want.
In order to send Ether from the Gatling Gun wallet, we must first send some Ether to it. Remember, the Gatling Gun has it’s own Ether, which it uses when you tell it to send Ether to somewhere else. So I charged the wallet with 2 Ether in this transaction. I also sent one ERC223 token to it in this transaction.
For the third transaction, we need some contract to call. I created a very simple contract that increments a single value each time it is called and emits an event. The contract with verified source code is here.
For the fourth transaction, we need a contract that can receive Ether payments in one of its functions. For that I created a simple contract that emits events every time it receives some Ether, along with the
uint256
value sent to it in the payload. The contract is here.So let’s create the target-value-payload triplets for each of these transactions.
For transaction 1, we will be sending the 1 Ether to one of my testing wallets:
0x785b8612b225b06764499f61e098725864ecd26b
. This will be the target. The value will be 1 Ether (1000000000000000000 Wei) and the payload will be empty (0x
). Together this adds up to:0x785b8612b225b06764499f61e098725864ecd26b -
1000000000000000000 -
0x
For transaction 2 we are actually targeting the token contract itself, which is
0x14c926f2290044b647e1bf2072e67b495eff1905
. The value in Wei will be zero, since token transfers are free. The payload is more complicated. The transfer function’s signature is 0xa9059cbb
, which will be the beginning of our payload. This is followed by a 256 bit representation of the recipient address, which we will set to my address:000000000000000000000000abcd412dd0e1b3a3bf1131e927450f71f2e9085a
This is then followed by the amount we wish to transfer, which is in our case 1 token (1000000000000000000 in wei):
0000000000000000000000000000000000000000000000000de0b6b3a7640000
Together, this transaction adds up to:
0x14c926f2290044b647e1bf2072e67b495eff1905 -
0 -
0xa9059cbb000000000000000000000000abcd412dd0e1b3a3bf1131e927450f71f2e9085a0000000000000000000000000000000000000000000000000de0b6b3a7640000
Transaction 3 is very simple. We call the contract at address
0xeeb66b5624ddfa13bee72d9e9dc418a34a74b5c5
, value will be zero and the payload will be the signature of the increment()
function, which is 0xd09de08a
. Together that is:0xeeb66b5624ddfa13bee72d9e9dc418a34a74b5c5 -
0 -
0xd09de08a
Transaction 4 makes use of all the possible inputs.We call the contract at address
0x06741096ef84fd751b0805a96583123b5cb11540
with a value of 1 Ether and call the payment(uint256 number)
function. Its signature is 0x8b3c99e3
and the payload we send to it can be any number, let’s say 123. Encoded into hex and put together the transaction looks like this:0x06741096ef84fd751b0805a96583123b5cb11540 -
1000000000000000000 -
0x8b3c99e3000000000000000000000000000000000000000000000000000000000000007b
We can append all these transactions to each other with a semicolon separator to end up with the following:
0x785b8612b225b06764499f61e098725864ecd26b - 1000000000000000000 - 0x ; 0x14c926f2290044b647e1bf2072e67b495eff1905 - 0 - 0xa9059cbb000000000000000000000000abcd412dd0e1b3a3bf1131e927450f71f2e9085a0000000000000000000000000000000000000000000000000de0b6b3a7640000 ; 0xeeb66b5624ddfa13bee72d9e9dc418a34a74b5c5 - 0 - 0xd09de08a ; 0x06741096ef84fd751b0805a96583123b5cb11540 - 1000000000000000000 - 0x8b3c99e3000000000000000000000000000000000000000000000000000000000000007b
There is nothing else we need to do with this text, we can simply copy and paste it into the transactions text field on the Operation page like this and hit “Add Transaction(s)”:
Once you add the transactions, you will see them all in the transactions list, which you can edit using the [remove] buttons.
All that’s left to do is hit Fire! I fired this exact sequence of transactions in this transaction on the Mainnet.
As you can see in the transaction overview below, both Ether transfers as well as the token transfer were registered.
The Event Log tab is the best place for investigating what happened during the transaction’s execution, as all our testing contracts emit events. The first two are the standard ERC223 token events. The last two are the events from our two test contracts:
At this point you may naturally want to ask “What makes this better than sending the transactions out one by one?”. There are three main reasons.
First of all, sending out transactions in bulk is cheaper. I have simulated each of our four transactions individually to demonstrate the difference in amount of Gas used.
If you look at the individual versions of Transaction 1, Transaction 2, Transaction 3 and Transaction 4, you will notice that their combined amount of gas used is 127 603.
Comparing this to the cost of the bulk transaction which cost 78 378 we can see that the bulk transaction was 38% cheaper.
This is because each Ethereum transaction has a base cost of 21 000 gas. This base cost is only applied to the bulk transaction once, no matter how many transactions it fires internally. In fact, the more transactions you fire at once, the more gas you save per transaction.
Second of all, sending out transactions in bulk gives us transactional safety. Suppose you sent the four transactions out as individual transactions. You must count on the fact that some of them may fail while the rest succeeds.
The bulk transaction enforces an all-or-nothing policy, meaning you can ensure that either all of your changes are applied or none of them are (and you can even modify any of them before a second try if you wish).
Finally, sending out transactions in bulk is more user-friendly. When designing a blockchain DApp one inevitably reaches the issue of having to bother the user with Metamask popups or any other means they use for blockchain communication.
This is inevitable for a true DApp that wishes to give the user full control. Bulk transactions allow the frontend of a DApp to “compose” and change the bulk transaction dynamically as the user interacts with the DApp and only ask the user to confirm all their changes at once after the user is done making them.
Also before I wrap up, some of you may be wondering whether the Gatling Gun wallet can be used to deploy contracts. After all, regular wallets can so why not the Gatling Gun wallet? Well, you’re in luck, because it can! There is a
deploy(bytes initCode, uint256 value)
function in each Gatling Gun wallet, which does exactly what you’d expect, but I really didn’t want to get into that in this article.This project and article are the result of my curiosity and desire to share interesting things with like-minded people out there. I hope you enjoyed the read. If this is something your project could make use of and you want to know more, check out the demonstration video on the ETH Gatling Gun homepage.
If you would like to contribute to this project in any way, I have placed the contract as well as the website code here on Bitbucket. The website code is very minimalist and self-contained (does not import any external scripts or CSS). This means anyone can run it locally simply by cloning the repository.