The Hard Cap Issue: Technical Explanation
My name is Simo, Tech Lead at Telcoin. For many, this will be my first interaction with you, and I regret that it has to be under these circumstances. Today, I have the difficult task of informing our ICO participants of an issue in our ICO smart contract. Let me start off by making it absolutely clear that this is not a security related issue. All ETH, BTC, fiat currency, and Telcoin are safe and remain liquid.
First, before I explain the issue in depth, allow me to summarize its impact on the token sale.
Summary
Due to a logic error in our ICO smart contract, we effectively reached the hard cap of our token sale earlier than should have been possible. After normal KYC refunds, this left approximately 8,500 ETH in our fast-track KYC contract that we are unable to directly transfer into the main ICO contract for token distribution.
While even without these ETH we are reasonably close to our actual hard cap, a significant number of our community members have been left outside the token sale, which we feel is deeply unfair considering their support for Telcoin. Telcoin is a community with a grand vision. To leave some members of our community out of that vision is unacceptable.
We have a way to correct this issue without replacing our ICO or token contracts, and to close the ICO the way it originally should have been. There will be no second ICO. However, this method comes with the downside of requiring all affected contributions that we could not transfer into the ICO to be refunded in their entirety. These community members will then be given an option to transfer up to their original contribution back into a new intermediary contract, a contract that will allow us to register the contributions in the ICO using a method that was originally meant for BTC and fiat contributions. These resubmitted contributions will go through in ETH. No funds need to be converted into any other currency.
This correction also requires us to delay token distribution by approximately three days. For an accurate timeline, please see our CEO’s post on this issue. At this point in time, no Telcoin have been distributed to any contributor.
We are fully aware that some members may choose to not transfer their ETH into the new contract, which is a choice that we willingly and respectfully accept. To be clear, from a purely business perspective, Telcoin is able to execute on its vision even without full re-commitment of these funds. However, our community has been, and continues to be, an immeasurable source of motivation for the entire team. We hope that many are still willing to embark on this journey with us.
So what exactly happened?
To date, we have raised over 32,000 ETH in our ICO. A significant portion of these funds have been stored in a low-friction, fast-track KYC contract. The fast-track KYC contract has enabled our team to eliminate delays in KYC processing by allowing participants to allocate their funds to the ICO in advance, locking in their bonuses and permitting our team to either approve or refund their contributions at a later time based on the result of said KYC process.
On January 6th, 2018 at 04:43:19 PM +UTC, while moving a batch of approved participants from the KYC contract into our main ICO contract for token distribution, an unexpected issue occurred where the ICO contract would no longer accept new participants¹, even though all had been approved. We quickly identified the issue to be that of the contract erroneously operating under the assumption that our hard cap had been reached.
The hard cap, as defined in the Telcoin whitepaper, is based on a fixed USD value of US$25,000,000. This meant that we had to account for the volatility of the ETH/USD conversion ratio in the contract. One way to do this is to use an oracle. An oracle is an external contract that provides access to external data, such as exchange rates, that smart contracts otherwise wouldn’t be able to access, and is updated by, for example, an automated script that runs periodically. Thus, oracles inherently require trust in the party that controls the script or program that updates the values. In many ICOs, any oracles being used are updated automatically by a script or program that runs on the cloud, e.g. AWS. For the Telcoin ICO, our view was that oracles implemented in this fashion add an incredible amount of complexity for what is in the end fully manipulable data. In the world of smart contracts, complexity is dangerous. Instead, we opted to use a much simpler method to keep our exchange rates up to date. Ironically, it is in the implementation of that method that the problem lies.
In the Telcoin ICO contract, both our soft and hard cap are implemented in two parts. The first part is a fixed minimum value defined in ETH that cannot be lowered. The second part is a flexible addition to the cap, that can only increase the effective value of the cap. The purpose of the fixed minimum part was to provide security to our contributors, by ensuring that there is a value that we simply cannot go under even in the unlikely event of a lackluster ICO. Not reaching the soft cap would have made the ICO contract enter refund mode at the end of the ICO.
At the start of our ICO, ETH traded at approximately US$425 per one ETH. Our soft cap, defined to be US$10,000,000 in the Telcoin whitepaper, was therefore roughly equivalent to a round value of 22,500 ETH at the time, and the hard cap of US$25,000,000 therefore 56,250 ETH at the same conversion rate. To account for a rise in the value of ETH, we chose to implement these caps as a 9,000 ETH fixed soft cap and a 22,500 ETH fixed hard cap, scaled up to 22,500 ETH and 56,250 ETH respectively with an effective scaling factor, or “cap flex,” of 250 percent. In essence, this meant that despite having a fixed minimum value for both caps, we would be able to handle an ETH value increase of up to 250 percent without having to end up raising more than we’d claimed to in the Telcoin whitepaper. While our prediction turned out to be downright scarily accurate, it is unfortunately that flexible portion of the hard cap that is the root cause of the issue at hand.
In the Telcoin ICO contract, the following three variables, taken verbatim from the actual contract code, define the effective soft and hard cap:
uint256 public softCap;
uint256 public hardCap;
uint32 public capFlex;
Let us now discard the soft cap and focus purely on the problematic hard cap. We check whether the hard cap has been reached with the following method, again taken verbatim from our actual contract:
function hardCapReached() public constant returns (bool) {
return weiRaised >= hardCap.mul(1000 + capFlex).div(1000);
}
This particular snippet of code is fine, and is used in various parts of the contract to check whether hard cap behavior should be active. Unfortunately, there is a single line of code in the ICO contract where this perfectly functioning piece of code is not used, though for a reason.
Below is the part of the ICO contract that contains the issue:
function buyTokens(address _beneficiary) saleOpen public payable {
require(_beneficiary != address(0));
uint256 weiAmount = msg.value;
require(weiAmount > 0);
require(weiRaised.add(weiAmount) <= hardCap);
This function is the one that both direct contributors and the fast-track KYC contract use when funds are transferred into the ICO contract. As can clearly be seen on the last line, an attempt is made to check that the new contribution does not bring us above the hard cap. Incidentally, that is also why the hardCapReached() method could not be used, as it is unable to account for the newly received ETH by itself. Crucially, this check does not include the cap flex calculation that was present in the functioning method we covered earlier. In effect, the check behaves just as if the contract was operating with no cap flex at all, with a fixed hard cap of 22,500 ETH. Since the minimum portion of the hard cap is not directly modifiable, it is not possible to change the result of this check, effectively permanently locking our hard cap to 22,500 ETH.
The Telcoin ICO contract has an extensive test suite that reaches full 100 percent test coverage. This means that every single line of functioning code is covered by a test, and every possible conditional branch has all of its conditions covered as well. We employ no fewer than 338 test cases in our token sale. Indeed, even the erroneous line of code is covered by our test suite no less than 92 times in a single run. It turns out that despite the borderline overkill test coverage, our suite did not have a test for the specific case of a token purchase after only the fixed minimum portion of the hard cap had been reached. Since everything functions normally until after that point, this issue went unnoticed until now.
For illustrative purposes, here is the single offending line once more:
require(weiRaised.add(weiAmount) <= hardCap);
An earlier version of the ICO contract was audited by a reputable member of the crypto community. Crucially, our implementation of the cap flex was a late game addition that was not included in the version that received the audit. Taking this shortcut was a choice that directly led to the issue and one that has caused us to disappoint our community.
Correcting an unfortunate situation
Though no more ETH can be directly transferred into the ICO contract, we have a way to correct the situation without any significant impact on the conclusion of the token sale.
Early on when designing the ICO contract, we realized that accepting BTC and fiat currency was going to be problematic. A BTC or fiat contribution at an unfortunate timing may have arrived safely way before hard cap was hit, but due to delays in processing and registering these contributions, new ETH contributions may have have brought the total contributions over the hard cap before our team even had the chance to register the BTC or fiat contribution. We therefore opted to not have a stringent hard cap check in the method that we use to register these contributions to the ICO contract, so that we could handle this edge case.
The functionality is generic and can also register contributions made in ETH, even though that is not its original purpose. We therefore have a way to give the community members we’ve wronged a second chance by the way of a new intermediary contract.
We are giving all who both passed KYC and sent their ETH before the true hard cap was reached an option to resubmit their contribution, but only up to their original contribution. This contribution will be sent to a new intermediary contract that, from a contributor’s point of view, functions just like our fast-track KYC contract did. Funds resubmitted using this method will keep their bonuses intact, based on the original transactions sent to the fast-track KYC contract.
Finally, I would like to personally apologize for the confusion, disappointment, and inconvenience caused by this issue.
Respectfully,
Simo
¹ Though the problematic transaction does not show up as failed on Etherscan, it has indeed failed. Due to the way our multisig wallet works, the failure has been logged as an ExecutionFailure event rather than the entire transaction failing.