Developing the X20 Auction Smart Contract – X20 Network – Medium
We set up some clear goals and priorities for the development. The main principles we followed while developing the X20 auction smart contracts were:
1. Security: Smart contract security is the main priority, as the safety of all users’ funds depends on it.
2. Usability: Using dapps creates an additional hurdle in itself as it requires a wallet, so we wanted to eliminate additional friction and make the auction as user-friendly as possible.
3. Efficiency: The gas cost for some decentralised exchanges far outweighs the actual trading fee, making trading a lot more expensive compared to centralised exchanges. Our auction approach already provides more efficiency because of its batched nature, but we wanted to optimise gas usage even further to make the gas cost negligible compared to trading fees.
One of the challenges for development was dealing with the timing requirements of running auctions. Auctions can be active and run for any number of hours, but there is also a set period at the end we call finalisation which is necessary to ensure that the correct price and orders are present.
To save on the gas cost of computing the optimal price — the price that maximises the trading volume — we implemented only its verification in the smart contract, so that any incentivised party can compute the optimal price themselves and suggest it to the smart contract, and the smart contract will be able to tell which suggestion is correct. This all happens in the Checking period.
To allow off-chain orders that cost nothing when the order is not filled — including no gas cost for the user — we have added a Reveal period where off-chain orders signed by the user can be submitted by the operator after filtering based on the price and knowing the final price in advance.
We have tried to keep the smart contract relatively simple to reduce the surface for possible errors. Because of this we have implemented all the components in a single smart contract with only the needed functionality. We have also added safeguards to prevent wrong auction execution even if the operator fails or transactions are spammed.
Thorough testing is also necessary to reduce the likelihood of errors. As such, we have tested many different setups with a variety of order types and combinations, different token types that do not exactly conform to the ERC-20 standard, server failure that would cause operator-only functions to not be called in time and a range of extra functionalities.
Having external experts look at the code is vital especially when the users’ funds is at stake and when the code handling them is immutable. We have engaged a third-party to perform a security audit of our smart contract. The audit found no critical issues and the issues it found have been resolved with the exception of two low severity ones. Those two are conceptual limitations that were known in advance and are part of the auction design, necessary for allowing off-chain orders and efficient price computation.
Batched transactions are often used to save gas costs for Ether and token transfers. One of the benefits of running auctions with many orders at once is that it can make use of this technique and greatly reduce the gas cost for any single order. It means that a single Ethereum transaction can submit potentially up to 100 orders at once, reducing the overhead of the transaction cost. However, it also allows us to do the order matching in O(n) time where n is the number of orders, by putting only the verification of the correct price on-chain based on the assumption that incentivised parties will submit the correct price suggestion even if the operator did not.
We have gone through the code very carefully looking at the gas used at every line to eliminate any unnecessary cost. One of the main conclusions is that storage is by far the most expensive operation that we frequently use, and the Solidity compiler makes surprisingly few optimisations to reduce that cost. We therefore decided to store every order in a single uint256 value which includes an id representing the user, the price for the order and the amount. We also added logic to keep storage values in memory if they need to be updated multiples times within the same transaction.
Another way in which we reduce the gas cost, by essentially arbitraging the gas price at different times, is by using what we call a gas reserve that works in a way very similar to GasToken. We can add to the gas reserve by filling up the storage at a time when gas prices are low, and then freeing up that storage and getting a gas refund when finalising the auction if the gas prices are higher. We use all 1s to fill the storage so that it is easily compressible by the client if needed.
In order to keep the user experience as smooth as possible, we have decided to support Ether and ERC-20 tokens directly, without requiring any wrappers such as wETH.
Some tokens do not follow the ERC-20 standard completely and have no return value for the
transferFrom functions. This has historically been ignored because of a bug in Solidity that was only fixed in version 0.4.22. Since we are using Solidity 0.4.24 — which has been audited by Zeppelin thanks to the Augur team — we cannot ignore this issue. We deal with these tokens specifically by defining
noReturnTransferFrom flags that help the smart contract select the right interface for the token.
We have also implemented some additional features in the smart contract — which are not yet accessible through the UI — that will further streamline the experience.
These features include the
depositAndBuy function that allows users to deposit some Ether into the contract and place a buy offer at the same time, so that they only need to confirm one transaction instead of two. The function also allows the smart contract to send the tokens back to the user directly once the auction executes without requiring a separate withdrawal transaction from the user.
We also support ERC-677 tokens by implementing the
receiveApproval function that essentially acts as a
depositAndSell function by utilising the data part of the
transferAndCall function which reduces the number of transactions as well.
To allow us to potentially distribute new tokens’ airdrops in the future, we have added a
claimNeverSupportedToken function that we can use to handle tokens that have never been supported by our smart contract and so could not have been deposited properly.
Users may have difficulty withdrawing their funds if they ran out of Ether on their Ethereum account and cannot pay for the gas, even if they have Ether or tokens stored in our smart contract. We have implemented a
withdrawForUser function that allows the operator to withdraw to a user’s wallet on their behalf — provided they sign a message with their private key — while accepting some Ether or ERC-20 token from the user’s account to pay for the gas cost.
We have intentionally made it difficult to upgrade the smart contract — to emphasise the immutable aspect of smart contracts for security — but for when we need to release a new version, we have created a
migrate function that allows users to transfer all their funds over to the new contract by making only one transaction.