Costs of a Real World Ethereum Contract

Written by djrtwo | Published 2017/07/09
Tech Story Tags: ethereum | blockchain | decentralization | cryptocurrency | smart-contracts

TLDRvia the TL;DR App

GAS PRICE PSA (2017–08–23): The median gas price at the time of writing this article was, and continues to be, in the realm of 20 Gwei. This is far greater than the typical average and safe-low found on EthGasStation (4 and 0.5 Gwei respectively). The median is so high because of bad gas-price defaults found in many wallets. I highly recommend using EthGasStation’s average gas-price or lower in order to not pay high fees and to help drive down the market rate for gas-price.

I previously discussed calculating costs of ethereum smart contracts by taking a look at low level operations called OPCODES in conjunction with the market rate for running those OPCODES (gas-price). The examples given were simple but a bit contrived so I decided to take last week’s analysis and apply it to an actual smart contract from start to finish.

I’m working on a series of simple smart contracts that are free and open for use. We’ll use the first one in this series — Escrow — and do a deep dive on the costs associated with it.

The Contract

A quick background on the contract up for analysis. This contract involves three parties — a sender, a recipient, and an agreed upon arbitrator. The sender initializes the contract with some amount of ether and specifies the recipient, the arbitrator, and an expiration date. If any of the two parties (sender, recipient, or arbitrator) confirm the payment via the [confirm](https://github.com/djrtwo/simple-contracts/blob/0a30f43929dcf6964b833348b62544977078cddb/contracts/escrow.sol#L35) function, the funds are released to the recipient. If two confirms are not made prior to the expiration, then the sender can void the escrow agreement and retrieve their funds via the [void](https://github.com/djrtwo/simple-contracts/blob/0a30f43929dcf6964b833348b62544977078cddb/contracts/escrow.sol#L47) function call.

The Costs

I deployed an instance of this contract onto the Rinkeby testnet which can be seen on Etherscan. This contract has three transactions associated with it, all of which use the gas-price of 20 Gwei which is the median price seen on Eth Gas Station.

  • The first transaction initializes the contract and deposits 0.5 ether into the contract. Transaction Cost: 0**.**01072934 Ether ($3.21 at $300/ETH)
  • In the second transaction the sender calls confirm. Transaction Cost: 0**.**00093492 Ether ($0.28 at $300/ETH)
  • In the third transaction the arbitrator calls confirm, and the funds are dispersed to the recipient. Transaction Cost: 0**.**00164754 Ether ($0.49 at $300/ETH)

If I were willing to wait about 20 minutes for my transactions to be picked up, I could have gotten these in on the main-net for 0.5 Gwei (according to Eth Gas Station’s “safe low”). At this gas price, deploying the contract costs around $0.07 and the other two transactions cost $0.01 or less at $300/ETH.

Considering this is a fixed cost for up to any amount of Ether held in this escrow contract, these fees seem reasonable. You would likely need to pay your arbitrator some amount to participate, but a lot of the costs and risk for the arbitrator (securely holding and dispersing funds) have been eliminated. You would have to pay solely for the arbitration — the decision — rather than all of the book keeping.

This is a great place to stop if you just wanted the straight up costs on a real world example. The rest of the article is a deep-dive on where exactly these costs come from.

Read on at your own risk!

Costs to Deploy

In solidity, the primary programming language currently being used for smart contracts, contracts are initialized via a constructor. You can see in the Escrow constructor that we pass in 4 pieces of data — the addresses of the three parties involved and a timestamp in the future at which the contract can be voided.

The initialization transaction for this contract is by far the most expensive operation. It required 536467 gas to deploy the contract and execute the constructor code. At 20 Gwei per gas, deploying the contract cost 0**.**01072934 ether, or about $3.21 USD at the current exchange rate of $300/ETH.

The Constructor

To further examine the initialization, let’s take a look at the VM trace of the transaction. This shows the OPCODES executed by the EVM in the constructor of the Escrow contract. This accounts for 113539 of the gas used. The most expensive operations were a number of SSTOREs which were used to store the addresses of the actors, the expiration timestamp, and to initialize some memory locations for the actors array and the confirmations mapping. These SSTORE operations alone accounted for 110000 gas, or about 97% of the gas used in the constructor. The rest of the OPCODES were used for checking the validity of the timestamp and business logic around getting everything stored.

What about the rest?

The constructor only accounted for about 20% of all gas used in this transaction. Where was the remaining 80% spent?

In this transaction, we specified a maximum gas allowed to be used (“Gas Limit”) as 1000000 gas. The EVM starts at this number and counts down with each operation to ensure there is enough gas remaining. If the EVM hits zero gas while in the middle of code execution, the transaction fails, changes are undone, and the fee associated with gas is still paid to the miner.

We can see in the VM Trace that we begin the constructor execution at 837872 gas remaining. This means at that 162128 (1000000-837872) or about 30% of the total gas has already been used when we get to the constructor.

What costs so much before we even get to the code execution? First, there is 21000 gas baseline transaction fee. This fee, called G_transaction_ in the Yellow Paper, is paid on all transactions in the Ethereum network. Next is an additional 32000 gas (G_create_) paid because this is a contract creation transaction. We’re at 53000 gas — still 109128 gas unaccounted for before the constructor. The bulk of this gas is used to pay for the size of the transaction data seen in the “Input Data” field on Etherscan. This is 3556 hex “nibbles” or 1778 bytes of data. As seen on page 20 of the Yellow Paper, G_txdatazero_ costs 4 gas/byte and G_txdatanonzero_ costs 68 gas/byte. We can calculate how many non-zero and zero bytes with the following equation — x * 68 + (1778-x) * 4 = 109128, where x is the number of non-zero bytes and 1778-x is the number of zero bytes. Solving for x gives x = 1594, so there are 1594 non-zero bytes and 184 zero bytes. As a sanity check, I wrote a python script to count the zero and non-zero bytes in the txdata. The numbers add up.

The remaining 50%

This still leaves about 50% of the gas being used after the constructor. Our gas limit started at 1000000. The last instruction of the constructor code left us at 724333 available gas. The transaction used 536467 gas in total so the available gas at the end of the entire transaction was (1000000–536467) 463533 gas. The amount available at the end of the constructor code less the amount available at the end of the transaction equals the amount of gas used after the constructor code finished — (72433–463533) 260800 gas.

At this point, we have paid for the transaction data and for the initialization of the contract, but what about future calls to the contract? Future calls will have to execute code, so that code must live on-chain in the state of the contract itself. Page 9 of the Yellow Paper discusses the cost of the “code deposit” to pay for adding bytecode to the blockchain state. cost = G_codedeposit_ * |o| where o is the “runtime bytecode” and G_codedeposit_ is 200 gas/byte. The runtime bytecode is the original bytecode sent in the transaction but stripped of the constructor and general initialization code. Because this initialization code is only executed upon initialization, it is extracted out to save you money and to save the node operators storage space.

The Solidity compiler outputs both the bytecode and runtime bytecode. Remix, an in-browser solidity editor/compiler, also shows you these two values under “contract details”. Remix is great for some quick info and sanity checks.

From the compiler output, I see that the size of the bytecode is 1650 bytes, while the size of the runtime bytecode is 1304 bytes. This implies that 346 bytes (1650–1304) are initialization code. The cost of the code deposit on the runtime bytecode is thus 1304 * 200 = 260800 gas which is exactly the amount we expected.

Again, the contract initialization transaction is by far the most expensive operation related to the Escrow contract and likely for most contracts. We are sending a transaction full of bytecode, running constructor code that initializes a number of places in memory, and then paying a fee for all of the code we are leaving on the blockchain in perpetuity. Increasing the size of the blockchain — the globally distributed and replicated database — is and should be expensive.

Costs to Confirm

Confirmation #1

The first confirmation on the Escrow contract simply updates the state to show the user confirmed. The money is still held and not released until the second confirmation.

This transaction had a gas limit of 90000 gas and used 46746 gas. Code execution began at 68728 gas and ended at 43254 gas, so running the code used 68728 — 43254 = 25474 gas. If you take a look at the trace, there is one SSTORE costing 20000 gas. This is to store the fact that the user sent in their confirmation. This is the majority of the code execution cost. The next most expensive operations are a number of SLOADs which load words from memory each costing 200 gas.

Because code execution began at 68728 of the original 90000 gas limit, 90000 – 68728 = 21272 gas was used prior to this. 21000 gas is the base cost for any transaction so there’s a remaining 272 gas to account for. Remember non-zero transaction data costs 68 gas/byte. This transaction had 4 bytes of data costing 4 * 68 = 272 gas.

21272 pre-execution gas plus 25474 execution gas equals 46746 gas, which is the total — all gas accounted for.

Confirmation #2

The second confirmation on the Escrow contract both updates the state to show the user has confirmed and releases the funds to the recipient. We expect this transaction to be a bit more expensive than the first confirmation because it is doing what the first did plus sending ether.

This transaction had a gas limit of 90000 gas and used 82377 gas. Code execution began at 68728 gas and ended at 7623 gas, so running the code used 68728 – 7623 = 61105 gas. The code execution started at exactly the same gas count as the first confirmation so 21272 gas was used prior to code execution, accounting for the 21000 base transaction cost plus 4 bytes of txdata. The amount used in code execution, 61105 gas, plus the amount used prior to code execution, 21272 gas, equals 82377 gas — all gas accounted for.

Let’s take a deeper look at the VM trace. Like the previous transaction, there is one SSTORE to store the fact that this user confirmed. The bulk of the remaining 41005 gas is used on an OPCODE named CALL. This operation used 32400 gas.

In the trace, it looks like 39981 gas is used on a CALL operation, but this is a bit misleading. This is actually just the amount of gas allotted to the CALL operation rather than the total consumed. Gas is allotted to CALL operations rather than simply spent because CALLs can execute code if the receiving address is a contract, and the exact amount of gas required to run the code is not known until executed. We can look at the operation before and after the CALL to calculate how much was actually used. The operation before the CALL ends at 40064 gas, and the operation after begins at 7664 gas. So the CALL operation sandwiched between uses (40064–7664) 40064 – 7664 = 32400 gas.

Let’s take a look at page 20 and 29 of the Yellow Paper to understand why the CALL operation costs what it does. Executing CALL costs 700 gas (G_call_) no matter what. Using CALL to transfer a non-zero amount of ether costs an additional 9000 gas (G_callvalue_). A CALL operation that adds new account to the blockchain state costs an additional 25000 gas (G_newaccount_). In this instance, the CALL did add a new account to the blockchain because the recipient address previously had no value or contract associated with it. A CALL operation with a non-zero value transfer also gets a stipend of 2300 gas (G_callstipend_) that is subtracted from the other costs associated with the operation. So in total

700 (G_call_) + 9000 (Gcallvalue) + 25000 (G_newaccount_) - 2300 (G_callstipend_) = 32400 gas

CALL gas is accounted for.

The expensive 25000 gas used for G_newaccount_ was a bit surprising. If the recipient had previously been a non-empty account, our gas costs would have been significantly smaller. When doing gas calculations/estimates there are a number of complexities like this depending on the current state.

Costs to Void

I’ll leave calculating and analyzing the cost of executing a successful [void](https://github.com/djrtwo/simple-contracts/blob/0a30f43929dcf6964b833348b62544977078cddb/contracts/escrow.sol#L47) transaction to the reader. We would expect this operation to cost a similar amount to the second confirmation because it does a value transfer via a CALL operation, but it should cost a bit less because it does not update the state of confirmations via an expensive SSTORE.

Further Notes

It should be noted that the expensive CALL operation in confirm likely should have been avoided entirely. Directly sending funds after an effect is actually an anti-pattern in solidity. The pattern is instead to update state via one function call that allows for a subsequent withdrawal. In the Escrow contract, the second confirmation should change the state of the contract to “confirmed”, disabling calls to void and enabling the recipient to call a new function named withdraw. This would avoid issues related to unexpected fees or operations related to the CALL.

The red flag in this analysis was the fact that we had an operation that could balloon in gas costs due to the unknown nature of what account/code would be called and executed. This unknown can lead to bugs such as the DAO reentrancy bug.

Smart contracts are incredibly powerful but, as we’ve seen, can be quite complex. The languages, tools, and best practices surrounding them are still in their infancy. I urge the community to focus a significant portion of its efforts on building out the underlying tools and architecture to ensure that we can create both correct and affordable smart contracts.

If we’re going to do this, let’s do it right.


Published by HackerNoon on 2017/07/09