Origin: Out of gas errors purchasing listing involving DAI swap

Created on 24 Jul 2019  ·  13Comments  ·  Source: OriginProtocol/origin

I got several out of gas errors attempting to purchase a fractional listing. I incremented the gas manually in metamask and eventually got it to work (it did take about 5 attempts though).

Additionally, once a purchase fails you can't attempt to purchase again as it immediately errors with a "See console" error and diplays "Expected event not found" in the console. This is probably because GraphQL gets confused by the failure.

Screen Shot 2019-07-24 at 2 20 19 PM

P1 dapp

Most helpful comment

Short summary:

  • Yes, we do have a problem
  • We need to add a 1% more ETH than quoted
  • We need to handle returning any extra ETH afterwords.

Yes, Uniswap works that way.

I'm now 100% sure that this is an issue. I cloned the Uniswap exchange vyper contracts, and modified the tests to check that it does indeed throw the transaction if you send event trillionth less eth than is required.

How often does this happen?

image

I also download the last 48 days of logs from the uniswap ETH-DIA contract. It's an fairly active contract. It averages a transaction every 10.4 blocks, or about 23.6 transactions an hour.

image

The buying and selling transactions were roughly evenly split. I'll just call it 50/50

Eth-DIA price chart, for fun.

image

What percentage do we need to add to be safe

Here's a histogram of the difference in prices between previous buy transactions:

It leans to one side, but I think that's because the market as a whole certainly made strong moves in a certain direction during this.

To know what we need to do to be safe. I absoluted the changes, then sorted them.

image

So in theory, adding 1% extra eth would handle 98% of price changes.

That's measured in prices changes though, not a percentage of our transactions. Our actual odds are better than that, since not every block contains a price change. But to actually calculate what percentage of our DIA purchases that would effect, we need to know how long it takes from when a user clicks "purchase", and signs the transaction, to that transaction being mined.

I don't have that data. If we assume 3 blocks as a best case, than our current code would work about 85% of the time. If that's 6 blocks, then success about 71% of the time.

After a 1% increase in ETH sent, then that goes to 99.5% success at 3 and 98.8% at 6.

Unfortunately

Our makeOffer transaction gas prices goes up by 20,000 gas per transaction (roughly a 6% increase).

However, then we will be getting a refund for every successful DAI buy. When using a proxy, this refund will go to the proxy. We'd need to modify our swapAndMakeOffer method on the identity proxy to send any refund back to the owner. This second money move will also cost us gas.

All 13 comments

From a contract perspective, what is different with fractional listings ?
Looking at the contract code, I don't see any difference in the code executed for making an offer on a fractional vs traditional listing... So why would it require more gas ?

CC @nick and @shahthepro since they have more background on fractional.

Fractional might not be relevant here, it might be just listings in general.

Yea no difference between fractional and non-fractional at the contract level. We probably just need to bump the amount of gas up a bit here. Which value worked for you, Tom?

Tried a few times with the default of 301785, bumped it to 401785 and still failed, then succeeded with 1001785. Probably not the most efficient. 😁

@tomlinton Was this on staging? No relayer, I'm assuming? Was this using proxy contracts?

It was mainnet, pretty fresh identity deployed with a proxy. Relayer doesn't get used for make offer calls afaik? You have to pay gas for ETH transfer anyway.

@tomlinton Can you get me a eth TXN hash for that purchase? Or discord message me your ETH address.

I'm fairly certain this is the DIA swap that is costing a bit extra.

I'm pulling the data to be able to know exactly what this cost really is, and how variable it is.

Turns out there may be a simple answer.

But stories with red herrings and wrong trails are more fun to read.

First I yanked down a set of 3,000 consecutive transactions from the DIA uniswap contract, and split them up by contract method to see what was being called, and how much they cost.

image

Then I broke that down to just the ones that accepted ether:

image

And finally just the function that we call:

image

Two interesting things here:

  • Buying DIA on the uniswap contract is reasonable. Either 55K gas or 75K gas - with a very bimodal distribution.
  • When it fails, it costs a little more.

No real answer here though.

Then I had a look at the @tomlinton's original transactions. The super interesting thing I see is that @tomlinton tried gas amounts of 301k, 301k, 301k, 401k, and 1001k, and yet the final succeeding transaction used only 296K. That should have fit into the gas amount of the first transaction. The problem then must be outside the gas amounts.

The uniswap exchange contract ethToTokenSwapOutput function works by taking ETH that you send, and a desired amount of tokens to buy. At the transaction time, the contract calculates the current price and number of tokens your eth can buy.

https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L173-L174

  • If you sent too much eth at the current exchange rate, then the contract gives you the tokens, and refunds your eth. This costs a little extra gas, which is why we see the two different success prices
  • If you sent exactly the right amount of eth, then the contract just gives you the tokens.
  • If you sent too little eth, then the exchange function crashes

Because of the way the uniswap exchange works, I think the exchange rate changes after each transaction.

When we calculate how much eth to send for DIA:

  const value = await exchange.methods
    .getEthToTokenOutputPrice(tokenValue)
    .call()

  const tx = exchange.methods.ethToTokenSwapOutput(tokenValue, deadline)
  return { tx, value }

We get the current exchange rate, but if any other exchange transaction happens between us getting the rate, and the transaction being mined, and it causes the rate to change in the wrong direction, it would cause our transaction to fail.

It's possible we may need to send a little extra ETH and expect a refund, or have a retry mechanism (boo).

I'll pull our relayer history and see if, and how often, this is happening, and if this is the real problem.

Short summary:

  • Yes, we do have a problem
  • We need to add a 1% more ETH than quoted
  • We need to handle returning any extra ETH afterwords.

Yes, Uniswap works that way.

I'm now 100% sure that this is an issue. I cloned the Uniswap exchange vyper contracts, and modified the tests to check that it does indeed throw the transaction if you send event trillionth less eth than is required.

How often does this happen?

image

I also download the last 48 days of logs from the uniswap ETH-DIA contract. It's an fairly active contract. It averages a transaction every 10.4 blocks, or about 23.6 transactions an hour.

image

The buying and selling transactions were roughly evenly split. I'll just call it 50/50

Eth-DIA price chart, for fun.

image

What percentage do we need to add to be safe

Here's a histogram of the difference in prices between previous buy transactions:

It leans to one side, but I think that's because the market as a whole certainly made strong moves in a certain direction during this.

To know what we need to do to be safe. I absoluted the changes, then sorted them.

image

So in theory, adding 1% extra eth would handle 98% of price changes.

That's measured in prices changes though, not a percentage of our transactions. Our actual odds are better than that, since not every block contains a price change. But to actually calculate what percentage of our DIA purchases that would effect, we need to know how long it takes from when a user clicks "purchase", and signs the transaction, to that transaction being mined.

I don't have that data. If we assume 3 blocks as a best case, than our current code would work about 85% of the time. If that's 6 blocks, then success about 71% of the time.

After a 1% increase in ETH sent, then that goes to 99.5% success at 3 and 98.8% at 6.

Unfortunately

Our makeOffer transaction gas prices goes up by 20,000 gas per transaction (roughly a 6% increase).

However, then we will be getting a refund for every successful DAI buy. When using a proxy, this refund will go to the proxy. We'd need to modify our swapAndMakeOffer method on the identity proxy to send any refund back to the owner. This second money move will also cost us gas.

I wish I could label comments as "Epic". 🏆 Great work @DanielVF. Thanks for digging into this!

Considering this done unless someone says otherwise ✅

Was this page helpful?
0 / 5 - 0 ratings