Creating the Smart Contract for the World’s First Interactive Coin Offering

By Enrique Piqueras and Clément Lesaege

If you’ve been keeping up with all that’s been happening at Kleros lately, you know that we recently launched the world’s first Interactive Coin Offering.

Interactive Coin Offerings (IICOs) are a new type of Initial Coin Offering (ICO), created by Vitalik Buterin (Ethereum) and Jason Teutsch (Truebit), that implement a set of economic incentives and penalties that level the playing field between participants of all sizes. We wrote an article that explains it in more detail here, in case you are curious. This post will delve into the details as to how and why we developed the IICO contract the way we did.

The Challenge

The IICO protocol, as described in the whitepaper, presents the following user stories:

  • As a user, I can place a bid in any of the sale’s phases and optionally set a personal cap on the total amount raised, in order to participate in the sale at valuations I am comfortable with.
  • As a user, I can withdraw before the withdrawal lock time with a penalty that is relative to the amount of time that passed since I placed my bid, so I can still get some of my contribution back if I change my mind.
  • As a user, I can redeem tokens and/or my refunded ETH at the end of the sale, to reap the fruits of my contribution.

We also decided to add an extra phase at the start of the sale where the bonus does not decrease so that everyone can take advantage of the full bonus and to avoid harmful network congestions.

The Solution

Clément Lesaege, our CTO, went to work with these goals in mind and returned with a very elegant and succinct solution. Let’s now go over its inner workings.

Initialization

The contract is deployed with the following parameters:

  • uint _startTime: The time the sale will start at.
  • uint _fullBonusLength: The length of the full bonus phase.
  • uint _partialWithdrawalLength: The length of the partial withdrawal phase.
  • uint _withdrawalLockUpLength: The length of the withdrawal lock up phase.
  • uint _maxBonus: The max/starting bonus.
  • address _beneficiary: The recipient of the amount raised at the end of the sale.

The ways these parameters are used to initiate the contract are pretty straightforward. The times are added up to calculate when phases start and end, and the other values are just persisted into storage.

After that, something a bit more complex happens. The bids in our implementation are stored in a sorted linked list. We sort it by personal cap and bid ID in ascending order to allow us to easily find which bids remain in the sale and which bids should be refunded. To avoid writing “if guards” that check if the list is empty in all the other functions that interact with it, we add two bids, the head (with contribution 0) and the tail (with the biggest possible integer contribution), into the list. These are “virtual” bids with no contributor, that are just there to simplify the rest of the code.

Displaying Data

All of the contract’s storage variables are public, which means things like the phase times can be queried freely. However, we did add two extra functions with the purpose of displaying data in user interfaces.

  • bonus: Calculates the bonus at the current time, taking the current phase into account. This is also used internally to calculate the bonus for new bids.
  • valuationAndCutOff: Performs operations similar to the ones performed when the sale is finalized. It finds the current cut-off bid (if the sale were to end at the current time), and returns 5 useful values: valuation, valuation with bonuses taken into account (for calculating tokens received and token prices), and the cut-off bid’s ID, contribution, and personal cap (for calculating the cut-off bid’s tokens received and price).

These, together with all the public storage variables, expose all the data that is needed to build a comprehensive user interface, and that’s exactly what we did with OpenIICO, a platform for the future of token sales, that we will delve into in a future post.

Submitting Bids

This is where the meat of the logic lies. It all revolves around the fact that the list is sorted and searching through it to find the correct spot to insert at, is an O(n) computation and could potentially cost a lot of gas after a significant amount of bids are made. We get around this by offloading most of the computation to a contract view, which costs no gas. There are 3 ways to submit a bid and 4 functions involved:

  • submitBid: This function inserts a bid into the spot specified. It will revert if the spot specified is not the correct one, so it is not meant to be used alone.
  • search: This function finds the correct spot to insert a bid in. It is a view, so it costs no gas. This can’t be used on its own with submitBid, because other bids might go through between the time you call submitBid and the time your transaction gets mined, which would make the result of search wrong. It also takes a starting position to search from, to enable the next function to do its magic.
  • searchAndBid: This function calls search and submitBid together, ensuring that the bid is placed successfully. The optimal way of bidding is to call search on the client, because it costs no gas, and then call this function with the result. This will enable the bid to go through even if other bids were made in between, because it calls search in the contract again. At most, this operation will be O(b), b being the amount of bids that are placed between the time you call search on the client and the time your transaction is mined.
  • Fallback (ETH Receiver): Sending ETH directly to the contract will place a bid with an infinite personal cap. This can be done with just submitBid, because bids are sorted by personal cap and bid ID in ascending order, so a new bid with an infinite personal cap is always going to go right before the tail.

This flexible design allows for users to place sophisticated bids with very little gas costs, and still allows for contributors to send ETH directly to the contract and not have to deal with a separate user interface than their wallet’s.

Withdrawing

Withdrawing is pretty straightforward and it’s handled by just one function, withdraw. Calling it in the correct phases will immediately transfer your refund to you and lock in the remaining part of your contribution to the sale, with a reduced bonus.

Finalization

This is the part where the final cut-off bid is found and the contract finalizes who has to be refunded and who has to be given tokens, or both when the cut-off bid is taken partially. It loops backwards through the entire list of bids, starting with the second last one, because the tail is not a real bid.

It keeps adding up all the contributions until it finds a bid that has a personal cap under the current running total, or that has a contribution amount that once added to the current running total, puts it over its personal cap. If the former, it refunds the entire bid, if the latter, it refunds the bid partially and accepts an amount such that the bid’s personal cap equals the running total. This running total then becomes the final valuation and is sent to the beneficiary.

The interesting part of this function is that it does not have to be called for the entire list at once. It caches the counters so it can be called by anyone, each specifying how many iterations of the loop they want to fund. This removes the dependence on a single party to finalize the contract.

Redeeming

Once the contract is finalized, anyone can redeem tokens and/or refunded ETH. Anyone can redeem a specific bid by calling redeem with the bid ID. It doesn’t have to be the bid’s contributor. We also built in the option of redeeming using the Fallback (ETH Receiving) function.

If you send 0 ETH directly to the contract, the contract will call redeem on all of your bids. This, combined with the last option in the “Submitting a Bid” section, allows contributors to participate in the sale and redeem, entirely from their wallet software of choice.

Extensions

For our ICO, we extended the contract we talked about here and implemented KYC functionality to comply with regulations. Similarly, you can extend the contract to support any custom functionality you need. That contract can be found here. We also built a multi-purpose open source user interface for the contract, that can be found here.

Audits

We worked with CoinMercenary and VeriChains to audit the code.

VeriChains

VeriChains auditors reported two issues that they considered vulnerabilities.

Incorrect ordering of bids for the same personal cap.

“Bids are ordered in ascending order by personal cap and then ID. This is enforced in the submitBid method by the ordering requirement.

require(_maxValuation >= prevBid.maxValuation && _maxValuation < nextBid.maxValuation); // The new bid maxValuation is higher than the previous one and strictly lower than the next one.

During finalization, the cut-off bid is found by traversing the list from the last bid to the first bid. This scheme incentivizes users to “spam” by re-submitting the same personal cap, because their new bid will be included first during finalization.”

This is not a vulnerability as the personal cap is the amount where a bidder is indifferent to whether its bid is included in the sale or not. In other words, if the valuation equals your personal cap, it should not matter to you whether your bid is accepted or not. Moreover, even if bidders wanted to have their bid accepted at their personal cap, for which they should have just submitted a higher personal cap from the start, they would just have to re-submit their bid, adding 1 Wei to their personal cap.

The change proposed by VeriChains was to sort by bid ID in descending order instead of ascending order. However, this would have significantly increased the amount of code in the contract, which would, in our opinion, increase the risk of vulnerabilities.

Since this comment about ordering was made 4 times by different parties, we updated the README to make it explicit that in the case of tied personal caps, the last one will be taken first.

Possible integer overflow in bonus method

“A bid’s bonus is calculated using constructor parameters and this might lead to unexpected behaviors when certain parameters are used.

/** @dev Return the current bonus. The bonus only changes in 1/BONUS_DIVISOR increments.
* @return b The bonus expressed in 1/BONUS_DIVISOR. Will be normalized by BONUS_DIVISOR. For example for a 20% bonus, _maxBonus must be 0.2 * BONUS_DIVISOR.
*/
function bonus() public view returns(uint b) {
if (now < endFullBonusTime) // Full bonus.
return maxBonus;
else if (now > endTime) // Assume no bonus after end.
return 0;
else // Compute the bonus decreasing linearly from endFullBonusTime to endTime.
return (maxBonus * (endTime — now)) / (endTime — endFullBonusTime);
}

In the above code, the multiplication maxBonus * (endTime — now) could overflow and return not-intended values.”

This cannot happen except for obviously malicious maxBonus values, and we explicitly stated that “The contract assumes that the owner sets appropriate parameters.”. A token sale providing something like a 1000000000000000000000000000000000000000000000000000000000% bonus, would obviously not be considered appropriate. In this case, the bonus would just become 0% would an overflow happen.

CoinMercenary

CoinMercenary auditors did not find any vulnerabilities.

“The reviewed smart contracts are well crafted and follow common security practices. No critical problems have been found. The Open IICO contracts are stellar examples of true craftsmanship. Working with contracts of this caliber is a rare experience.”

Bug Bounties

We set up bug bounty programs of 10 ETH and then 50 ETH maximum payouts and put them on Solidified. Two issues were found thanks to the bug bounties:

  • The manual withdrawal penalty was ⅔ of the bonus instead of ⅓ as described in the paper. The code was matching our documentation, but our documentation was not matching the paper.
  • The redeem function was not checking the return value of the token’s transfer function. We use the MiniMeToken, so it would not have been problematic in our case, but it could have caused issues for people using our code with old token contracts.

Both of these were corrected before the start of the sale.

Do let us know if you have any questions about the implementation or if you come up with any improvements or suggestions. As always, you can reach us at one of the channels below.

Learn More

Join the community chat on Telegram.

Visit our website.

Follow us on Twitter.

Join our Slack for developer conversations.

Contribute on Github.