The Unilayer Disaster — A post mortem deep dive

SAFU Auditing
8 min readFeb 8, 2021

In early December 2020, the UniLayer launchpad held its first-ever token sale. After the decline of competitors like LID and Bounce the market was primed for a good, solid stable launchpad system to be a success.

The telegram was buzzing, people were excited to jump into the first presale on the UniLayer platform and the UniLayer team was excited to show off their brand new tech.

As we moved closer and closer to the time of the sale the crypto Twitter and youtube environments were primed, with everyone wanting to jump into this BetFi Sale.

BetFi is a decentralized gaming platform, offering games such as poker live on-chain. Prior to the sale, they had already deployed a substantive test on the ethereum test net. People were ready, they had tested the platform and were excited to buy a chunk in the sale.

Then the sale opened, aside from some small website glitches it was relatively successful. People bought in, the eth number kept rising, and eventually, the soft cap was met. Cue excitement in the telegram, moon boy talk began again and everyone was pumped for what this meant for UniLayer and its future.

There was a brief interruption worth mentioning, the launchpad contract sale was changed mid sale. This caused some confusion and a mild panic but was resolved with little to no effect on the sale. This contract change DID NOT impact what happened next.

Then came the pivotal moment in any sale, time to take the eth out and begin the process of adding liquidity, and distributing tokens. This is where everything fell apart. The team ran their “setupLiquidity” function, which is used to automatically add tokens to uniswap and begin the withdrawal process of the eth. However, it failed. “Out of gas” was all the mainnet spat back as an error.

You can see the first failed transaction here:
https://etherscan.io/tx/0x4a4a7ff27aa573f6edce883dc4828f391edff63c00e7e4da81fac6656197e87f

Immediately after the team attempted a further 5 times. They attempted to increase the gas limit substantially, attempted to withdraw from another owner’s account, and tried anything they could think of to get this ether out.

But, as is the case with ethereum, once it’s stuck…it’s stuck.

Panic hit the social groups, people slowly began to realize that the team couldn’t access the funds. Eventually, the team put out an announcement letting people know that there is some error with withdrawing and they are working on it.

As of writing this article, the ether is still stuck in that contract. From all my analysis on the subject, that ethereum will never be retrievable and the $1,341,338.34 is stuck forever. Let’s dive into why this ether can not be retrieved and how this error came to be.

You can find the contract and follow along with the code here; https://etherscan.io/address/0xa205d797243126f312ae63bb0a5ea9a32fb14f41#code

As mentioned previously, the function to withdraw is the setupLiquidity() function. We are going to dissect this line by line, if you want to see the final result just jump to the end of this article. A bit of background knowledge, when a contract function errors with “out of gas” and the user has set a high gas limit, 9 times of out 10 it’s because a require or a send functionality within the method has failed.

Usually, it’s because an address doesn’t exist or a required check fails. So with that knowledge, let’s go line by line and determine where the error occurred before we dive into how the team missed it.

The contract setup is simple, no inbound variables a public flag, and an only owner flag. This means it can be called externally and only the owner of the contract is able to call the function. This stops users from ever being able to run the finalized function.

NOTE: This is an interesting thing to add, you don’t need the owner to be the only runner if you have the right checks and automation. It should be irrelevant who runs the function as everything is predetermined to go where it needs to go.

Now let’s look at the first three requirements. We can run these variables by accessing the contract directly, some of these are variables stored with mappings and structs but you can find most of them by going to: https://etherscan.io/address/0xa205d797243126f312ae63bb0a5ea9a32fb14f41#readContract

And then selecting “getDetails”. The sale is not sold out, but the block timestamp is bigger than the _end date (AKA the current time has passed the end time meaning the sale has ended) so the first required check passes. The second requirement is that isRefunded is false, aka the sale has not been refunded. This is indeed false making the second check pass.

The third requirement is that isLiquiditySetup is false, this is also the case making the third requirement pass.

So far so good.

The next snippet is some actions, it immediately sets isLiquiditySetup to true, and then if the raised ether amount is less than the preset soft cap it will mark it as refunded and exit this function.

Interestingly it doesn’t trigger a refund, just marks it as refunded presumably for the team to trigger the refund actions. None of this will cause an error, and since the raised ether is indeed greater than the soft-cap the function will continue to execute.

The next snippet is saying make a variable called ethBalance equal to the ether balance of the contract itself and then make sure that it is greater than 0. It is greater than 0, hence the locked funds, so this check passes.

This is a bigger snippet as it’s mainly setting variables that will usually pass on ethereum as it would “normally” fail on compile if the variables were inaccessible. That means it’s quite hard to deploy a contract where it’s setting a variable it can’t.

Unless it takes input variables, but in this case, it does not. So line by line; We set the liquidity amount to be the eth balance of this contract multiplied by the liquidity percent and then divided by the total percent variable. This comes out to a positive integer so no failure there.

We then set a variable, tokensAmount, to be the betfi token balance of this contract. Nothing fancy, no failure.

We then check that the tokens we have in the contract are more than the liquidity amount of tokens we are trying to add. It is, we’re good so far!

Then we set the team ether amount, layer fee amount, support fee amount, and stake fee amount variables equal to our respective percentages of raised ether.

Nothing really interesting, it’s just predetermining how much we are going to be taking as a fee, sending to the team, and adding to the stake contract amount.

As long as all of these equal less or exactly the amount of ether in the contract we won’t have any issues. If they equaled more than the ether in the contract we wouldn’t be able to send them causing an error.

This is not the case.

Now we get to the nuts and bolts of the contract. We’ve already done all of our pre-checks with no errors and now need to start sending the money around.

First we send it to the layer fee address which is a hardcoded address; 0xa6A7cFCFEFe8F1531Fc4176703A81F570d50D6B5.
We send the fee amount, no big deal seems to work fine.

Then we sent our support fee to the support fee address (“0xD3cDe6FA51A69EEdFB1B8f58A1D7DCee00EC57A8”) and again no big deal works fine. Then we send our stake fee to the stake fee address, and we see the out of gas error.

The stake fee address is “0xfB5B0474B28f18A635579c1bF073fc05bE1BB63b”.

If we look this up on etherscan it looks relatively simple except for one huge difference. It’s a contract, not an address. As most people in crypto will know, sending ether or tokens to a contract address is always risky.

Most often, it’ll lose that balance with no way of retrieving it. Luckily, in new solidity versions, people have begun adding a fallback option to their contracts. This tells the contract what to do in case someone sends directly to it.

In the case of the unilayer staking contract the fallback options is;

Hmm, it tells the contract that when ether is sent directly to it add the send amount (msg.value) to the variable ethtonextstake.

It’s incrementing a number. This costs more gas than a standard eth send as you’re increasing a variable. That’s our issue.

When running the setupLiquidity function, the user is adding predetermined gas for it to be run. The developers of UniLayer HAVE NOT accounted for extra gas being needed when sending to the staking contract (or any contract usually). So, the function runs out of gas as it can not pay that function. There’s no way to increase the amount of gas being sent without changing the code, which you can not do on a smart contract.

So no matter what we do, the setup liquidity function will fail and that ether can not be retrieved. It is locked forever. The question is, how was this not picked up in testing.

The answer, they did not test sending to a contract. If you go and look at the tests the developers ran on ropsten (https://ropsten.etherscan.io/address/0xf770eb63d4cf9c041971f89b475cb1bf5a873b12) you will find that the addresses they set as the receivers were all addresses and not contracts.

If they had tested sending to a contract on ropsten, they would have received the exact same error as on mainnet. They did not do this. On top of that, they did not test on mainnet before running the betfi sale.

Which is absolutely ludicrous. Because of this, that money is locked forever.

If most of this article confused you, the simple version is this. The unilayer launchpad code was released on mainnet and not tested.

The code had a bug, sending ether to a contract that was an ever failing transaction causing the money to be irretrievable from the contract. As of writing this article, the unilayer and betfi teams are still working on a solution to counteract this issue and have been very transparent as to what occurred. All of this was visible on-chain before the sale, our hope through this article and others is to drive adoption of “testing before purchasing”.

We hope to educate our audience to have the skills necessary to inspect code like this and determine it will fail BEFORE sending their ether to it.

Written by NoFeisu, Lead Auditor at Safu Auditing

--

--

SAFU Auditing

The trusted source for smart contract auditing. Keep up to date with our audits as we complete them.