Estimate Gas when using Oraclize

September 12, 2017

Ethereum transactions require you to pay a fee that is measured with gas. Every instruction executed by the EVM costs a certain amount of gas, so before queuing your transaction, you have to specify a gas limit and a gas price. That gas is paid in ether, so for example, if your transaction has a gas limit of 82470 and your gas price is, say, 1 Gwei (0.000000001 ΞTH), then the cost of the transaction would be:

82470 x 0.000000001 = 0.00008247 ΞTH

If your transaction spent more gas than your gas limit, it gets reverted (i.e., changes to the contract state are not applied, although the transaction is kept in the blockchain), and you lose the ether spent on it. This is by design, and it is meant to avoid spam in the chain: spamming it will cost you real money.

Good news is that if the transaction is successful, you will get refunded with the amount of gas that was not spent. In the example above, let’s say the transaction used 54980 units of gas, then you would get refunded:

0.00008247 - (54980 x 0.000000001) = 0.00002749 ΞTH

In other words, the actual transaction cost was 0.00005498 ΞTH. The lesson here is: it’s OK to overestimate the gas limit because you will be refunded any extra gas not consumed.

Oraclize

The EVM is deterministic by design. This means, for example, that there is no way to generate a random number because to reach consensus each miner has to get the same result after executing the contract’s code. That’s also why there is no way to do HTTP calls from within a smart contract.

So we need “oracles”, which are 3rd party services that allow you to introduce randomness or external sources data into the blockchain.

oraclize image

Oraclize is one of the most popular oracle services. The idea is simple:

  • You make your contract extend a super contract provided by them
  • You make queries to data sources (e.g. “URL”, “Random”, “WolframAlpha”, and more), which are transactions made against Oraclize’s contract
  • They detect queries, perform them, and call your contract back
  • The callback in your contract receives the requested data, and then you do something with it

This is the cannonical example of their product:

pragma solidity ^0.4.11;
import "github.com/oraclize/ethereum-api/oraclizeAPI.sol";

contract ExampleContract is usingOraclize {

    string public EURGBP;

    function __callback(bytes32 myid, string result) {
        if (msg.sender != oraclize_cbAddress()) throw;
        EURGBP = result;
    }

    function updatePrice() payable {
        if (oraclize_getPrice("URL") > this.balance) {
            // "Oraclize query was NOT sent, please add some ETH to cover for the query fee
        } else {
            // Oraclize can be queried
            oraclize_query("URL", "json(http://api.fixer.io/latest?symbols=USD,GBP).rates.GBP");
        }
    }
}

Which brings us to their pricing model: before you query their contract, you need to make sure you have enough ether in your contract to cover the fee. Prices depend on the type of query (e.g. 0.01 USD for URL). But there is more to it: because Oraclize will call your contract back, that transaction will also consume gas, so you must pay for that upfront when you call oraclize_query.

There is big caveat here, from their docs:

If no settings are specified, Oraclize will use the default values of 200,000 gas and 20 GWei. … Smart contract developers should estimate correctly and minimize the cost of their __callback method, as any unspent gas will be returned to Oraclize and no refund is available.

In other words, overestimating the gas limit means you will lose money. And that is neither good nor fair, but it seems like there’s no way around it, except of course to estimate correctly.

Estimating gas limit

web3.js comes with a handy function called estimateGas that executes a message call or transaction, which is directly executed in the VM of the node, but never mined into the blockchain and returns the amount of the gas used.

Now, that seems like something very useful. But be aware that the estimated gas could change depending on many factors, like the current state of the contract or the input values of the function.

Let’s take a look at an example. Imagine you have an array in a smart contract that you want to shuffle. So we could use the KFY algorithm to do this, except that we do not have a way to generate random data from within the smart contract. We could, however, use Oraclize to provide us with a random string that we can then use to effectively introduce entropy in the contract. Let’s see the code:

function sendQuery() {
    require(this.balance > oraclize_getPrice("URL"));
    oraclize_query("URL", "https://www.uuidgenerator.net/api/version4");
}

function __callback(bytes32 myid, string seed) {
    if (msg.sender != oraclize_cbAddress()) throw;

    uint[] memory deck = /* the array you want to shuffle */;

    shuffle(deck, seed);

    // do something with deck
}

// Run KFY
function shuffle(uint[] arr, string seed) {
    for (uint m = arr.length; m > 0; m--) {
        uint rand = uint(keccak256(seed, m)) % m;
        uint temp = arr[m - 1];
        arr[m - 1] = arr[rand];
        arr[rand] = temp;
    }
}

This would work just fine except, of course, for the fact that you would spend around 0.0040336, non-refundable ΞTH:

# oraclizeFee (as of this writing) + oraclizeDefaultGasLimit * oraclizeDefaultGasPrice
0.0000336 + 200000 * 0.00000002 = 0.0040336

There are two ways to save money here:

  • Reduce the gas price
  • Correctly estimate the gas limit

The former is very easy to achieve:

function TheConstructor() {
    ownerOfContract = msg.sender;
    oraclize_setCustomGasPrice(1000000000 wei); // i.e. 1 GWei
}

The latter requires a structural change to the contract. oraclize_query receives as a third parameter a custom gas limit. Note, however, that in this particular case we can’t just hardcode the gas limit because depending on the size of the array we want to shuffle, we can consume more or less gas.

So we want to provide the gas limit to the oracle query dynamically:

function sendQuery(uint gasLimit) {
    require(this.balance > oraclize_getPrice("URL"));
    oraclize_query("URL", "https://www.uuidgenerator.net/api/version4", gasLimit);
}

Let’s then, change the contract code a little bit to allow us to estimate the cost of the transaction safely:

function __callback(bytes32 myid, string seed) {
    if (msg.sender != oraclize_cbAddress()) throw;

    myFunction(seed);
}

function estimateCostOfMyFunction(string seed) {
    if (msg.sender != ownerOfContract) throw;
    myFunction(seed);
}

function myFunction(string seed) internal {
    uint[] memory deck = /* the array you want to shuffle */;

    shuffle(handsOrder, seed);

    // do something with deck
}

We abstracted the code of the callback into myFunction and added estimateCostOfMyFunction which should consume almost the same gas as the oracle’s __callback. Then, from our dApp we could have something like this:

// Here I'm assuming you use truffle, see below for an example of how to get the contract instance
var contractInstance;
App.contracts.MyContract.deployed()
    .then(function (instance) {
        contractInstance = instance;
        // estimate the cost 100 times using different seeds
        var estimates = [];
        for (var i = 0; i < 100; i++) {
            var seed = Math.random() + "";
            var estimate = instance.estimateCostOfMyFunction.estimateGas(seed);
            estimates.push(estimate);
        }
        return Promise.all(estimates);
    })
    .then(function (estimates) {
        // choose the max estimate
        return Math.max.apply(null, estimates);
    })
    .then(function (estimatedGas) {
        // actually send the query with the estimated gas (+ 2% extra gas just in case)
        return contractInstance.sendQuery(estimatedGas * 1.02);
    });

Basically, we use estimateGas to simulate our function several times with different inputs and choose the highest amount of gas that could be consumed plus 2% more, just in case.

But, but… I’m not using truffle

Ok, ok. So you could do this with plain web3:

var contractABI = [ ... ];
var contractAddress = '0xxxxx';

var web3conn = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));
var contract = web3conn.eth.contract(contractABI);
var contractInstance = contract.at(contractAddress);

var estimatedGas = contractInstance.theFunction.estimateGas('args of function');

© 2017 | Powered by Hugo