Hacking a Popular ICO Practice That Only Rewards the Richer

Written by pabloruiz55 | Published 2017/11/24
Tech Story Tags: ethereum | smart-contracts | ico | solidity | tutorial

TLDRvia the TL;DR App

If you have been part of the Ethereum / Cryptocurrency ecosystem for at least a few months, you might be aware of the ICO craze going around.There’s literally dozens of ICOs / Token Sales springing out every day as can be seen on popular platforms — like IcoAlert or TokenMarket— tracking ICO activity.

One of the most common practices used by ICO teams to promote and incentivize their token sales is to offer hefty discounts or bonuses to contributors based on when they buy into their tokens or how much they are willing to invest.

Why should the rich get richer?

Recently, there’s been some hot debate whether or not it’s ok for ICOs to offer bonuses based on volume purchased / money spent.

Detractors of this practice argue that it goes against the spirit of Ethereum — And crypto in general — to reward people with more resources to gain an advantage over other people with less resources.

Accessing Volume Discounts by Resource Pooling

Volume-based bonuses won’t be going away anytime soon. So… How can we take advantage of these bonuses if we can’t (or won’t) commit the minimum ether the ICO team is asking for?

One of the ways to do this would be to find a few friends that are also interested in contributing to a particular ICO and have them send their ether to one of the members of the group, have him buy the tokens and then distribute the acquired tokens accordingly among this group of friends.

Love, Faith and Hope. These are much needed when investing together with friends. (Photo by Jonathan Brinkhorst on Unsplash)

In theory, this approach is pretty easy and straightforward, but in practice, it can become problematic. For one, you would have to trust this person receiving everyone’s ether to come through, which already is a risky proposition. What if this person disappears with the money? What if he forgets his private key? What if his private key gets stolen? What if he makes a mistake and sends the money to an incorrect address? There’s so many things that could go wrong.

Also, what if we don’t have enough friends that are interested in investing with you in a particular ICO? You might still want to do it and pool the money with complete strangers. There are a few sites and forums where people can find other people to invest together, but this presents a huge risk for everyone involved. I wouldn’t send money blindly to someone I don’t know extremely well, and neither should you.

So, how can we collaboratively invest on an ICO without having to trust anyone else with our money? With a smart contract, of course! 📄

Building an Investment Pool Smart Contract

In the following paragraphs I’m going to describe how to build a smart contract that allows several contributors to send money to it; have it invest in the selected ICO when some pre-conditions are met; and finally have it distribute the tokens purchased on a pro-rata basis.

This smart contract would act as an intermediary that collects the money from several accounts and then forwards the collected funds to an ICO contract in one transaction in order to gain access to the volume-based bonus offered by the team.

The tokens would then be bought by the contract, which allows the contributors that participated in the investment pool to withdraw them to their own accounts.

Let’s take a look at the smart contract that allows all this.

But first, a few disclaimers, warnings and considerations:

  • Before using the following contract to create an investment pool for an ICO you should make sure said ICO will allow you to later withdraw the tokens. Some ICOs don’t use an smart contract while collecting contributions for example. Some ICOs also have whitelist mechanisms in place and KYC steps that need to be completed that might prevent a contract from investing on behalf of a physical person.
  • The ICOPool contract has no way to know the details of the ICO it is targeting since there is no standard protocol to crowdsale contracts. This means that, for example, it doesn’t know if there’s a deadline for the bonus or what’s the minimum / maximum contribution amounts, or if the ICO has already finished, etc. So, the contract relies on its admin to properly configure it and trigger the token purchase at the right moment.
  • The ICOPool contract ends up participating in the ICO by making an external call to the crowdsale contract, executing its fallback function. Most crowdsale contracts are prepared to execute a buyTokens function(or a function with a similar name) upon receiving ether without any other data. You should make sure the ICO you do the investment pool for works this way.
  • It goes without saying that the tokens being bought must be ERC20 compliant. If they are not, you won’t be able to guarantee that the tokens can be transferred from the investment pool to the contributors.
  • The following code has not been tested yet in a real-life project. Use it under your own risk, I’m not to be held responsible for its improper use. Also, make sure the ICO you choose for the investment pool is not a scam. Using this contract has the same risks as if you would invest in the ICO by sending ether to it, this contract adds no additional security mechanisms. Proceed with extreme caution.

The ICOPool Smart Contract

You can find the complete source code in my Github repository. Please take a look at the README.md file for further usage instructions, more in-depth information and examples.

The ICOPool contract has 4 parts to it:

  1. Configuring the investment pool.
  2. Receiving ether from contributors to form the pool.
  3. Investing in the target ICO once the pool’s requirements have been met.
  4. Allowing the contributors to obtain the acquired tokens.

Configuring the investment pool

In order to put an investment pool in place, first we have to deploy the contract with a few parameters. The account deploying the ICOPool contract will become its admin. The admin’s only purpose is to execute the function that buys into the ICO.

Here’s the code involved in the creation of the investment pool.

function ICOPool(address _targetICO,uint _minContribution,uint _maxContribution,uint _poolSoftCap,uint _poolHardCap,uint _contributorsSoftCap,uint _contributorsHardCap) public {

    require (\_targetICO != address(0));  
    require (\_minContribution > 0);  
    require (\_maxContribution > \_minContribution);  
    require (\_poolHardCap > \_poolSoftCap);  
    require (\_contributorsSoftCap > 1);  
    require (\_contributorsHardCap >= \_contributorsSoftCap);  
      
    // Max people \* their minimum contribution should be able to meet pool softcap  
    // For example, we can't allow having the max contributors (10 people) put $100 each when the softcap is $1500.  
    require(\_contributorsHardCap \* \_minContribution >= \_poolSoftCap);  
    // Min people \* their maximum contribution should be within pool hardcap  
    // For example, we can't allow 3 people to reach the hardcap if the minimum contributors is 5  
    require(\_contributorsSoftCap \* \_maxContribution <= \_poolHardCap);  
      
    targetICO = \_targetICO;  
    poolAdmin = msg.sender;  
    minContribution = \_minContribution;  
    maxContribution = \_maxContribution;  
      
    poolSoftCap = \_poolSoftCap;  
    poolHardCap = \_poolHardCap;  
      
    contributorsSoftCap = \_contributorsSoftCap;  
    contributorsHardCap = \_contributorsHardCap;  
}

The contract receives quite a few constructor parameters:

  1. address _targetICO: This is the ICO the investment pool will contribute its funds to. Anyone using this contract should double check this variable is pointing to the ICO the organizer says it does.
  2. uint _minContribution & uint _maxContribution: These are the minimum and maximum amount of ether (expressed in wei) that the contributor to the investment pool can put into the pool.
  3. uint _poolSoftCap & uint _poolHardCap: These are the minimum and maximum amount of ether (expressed in wei) that the investment pool allows in order to be able to invest in the target ICO. The soft cap must be reached before the admin can buy the tokens from the ICO. And no contributions may be accepted once the hard cap is reached.
  4. uint _contributorsSoftCap & uint _contributorsHardCap: These are the minimum and maximum amount of contributors the investment pool may accept.

Notice there’s a complex relationship between all these parameters so we don’t risk the chance of the investment pool getting stuck:

  • The soft cap (_poolSoftCap) should be lower than the max amount of contributors (_contributorsHardCap) multiplied by their minimum possible contribution (_minContribution). This way, it is not possible for the ICOPool to reach full capacity in terms of people but not reach the minimum ether goal. (If 10 people is the maximum number of contributors allowed, they should not be able to only invest 1 eth each, if the soft cap is greater than 10 eth).
  • Along the same lines, the hard cap should be higher than the minimum amount of contributors multiplied by their maximum possible contribution. This way, it is not possible for the ICOPool to reach full capacity in terms of ether but not reach the minimum people required. (2 people should not be allowed to reach the pool’s hard cap if the minimum people was 3 or more).

It’s also extremely important to notice that the ICOPool contract has no way to know what the minimum contribution is in order to trigger the bonuses. It’s up to the admin to correctly set the soft and hard caps accordingly.

Receiving ether from contributors

Once the investment pool is set up, the person who created the contract can start promoting it among other people to pool contributions.

Sending ether to the investment pool is done the same way as one would with a crowdsale. Just transfer the ether to the given address and the contract will take care of it.

*It’s worth mentioning that the contributor should take the same measures he takes when investing directly on the crowdsale. For example, they must send the money from an account they own the private key of.

Here’s the code that gets executed when someone sends money from their account.

function() payable public {require(msg.value > 0);require(msg.value >= minContribution && msg.value <= maxContribution); // Must send eth within min and max contributionsrequire(contributorsBalance[msg.sender].add(msg.value) <= maxContribution); // msg.sender's balance can't exceed max contribution limit

    // Pool can't exceed hard cap  
    require(poolBalance.add(msg.value) <= poolHardCap);  
      
    //Register how much eth the pool has  
    poolBalance = poolBalance.add(msg.value);  
      
    //If it is the first time this account contributes, increase num. of contributors  
    if (contributorsBalance\[msg.sender\] == 0){  
        amountOfContributors++;  
    }  
    // Pool can't exceed contributors hard cap  
    require(amountOfContributors <= contributorsHardCap);  
      
    //Register how much eth has each contributor put into the pool  
    contributorsBalance\[msg.sender\] = contributorsBalance\[msg.sender\].add(msg.value);  
}

First, we make sure that the contribution being made is within the boundaries the admin defined and that the people/money hard cap is not exceeded.

If the contribution is accepted, we add the money sent to the contributor’s balance. The contributor doesn’t have to put all the money in one go, but each contribution should meet the minimum set by the admin.

Investing in the target ICO

At any point in time — as long as the minimum requirements are met — , the admin may decide to stop collecting funds and make the purchase of the tokens from the ICO.

Once he does, the investment pool contract may no longer accept incoming transfers of ether.

As I mentioned before, this step must be performed by the admin, although the contract could be modified to allow any of the contributors to do it. This other approach makes the contract less dependent on one person, but it also makes it more messy and prone to problems. The main issue with allowing anyone to buy the tokens has to do with the fact that this ICOPool contract has no way to know the inner workings of the target ICO. In order to avoid conflict, the admin is the one that decides when to buy the tokens, and responsible for it. For example, the contract has no way to check if the volume bonus promoted by the ICO team is active at the moment, so it’s up to the pool admin to check that and move forward with the token purchase. — Or not doing anything so the contributors can withdraw their funds — .

function buyTokensFromICO() public {require(!investedInICO);require(poolBalance >= poolSoftCap);require(amountOfContributors >= contributorsSoftCap);require(this.balance >= poolBalance);

    //Can be called only by the pool admin to avoid timing problems  
    // We'll need to trust the admin to execute this at the right moment  
    // Could be changed to allow any contributor to call it.  
    require(msg.sender == poolAdmin);  
      
    investedInICO = true;  
      
    // BE CAREFUL, OPENING RE-ENTRANCY DOOR  
    require(targetICO.call.value(poolBalance)());  
      
    // \*\*\*\*\*\*\*\*\*\*\*\*\*\*  
    //If you are hesitant about using call() you can instead instantiate  
    //the target ICO and directly use whatever function it has to buy tokens  
    // ------  
    //Crowdsale c = Crowdsale(targetICO);  
    //c.buyTokens.value(poolBalance)();  
    // \*\*\*\*\*\*\*\*\*\*\*\*\*\*  
}

Sending the funds to the target ICO is pretty straightforward. Once we have checked that the soft caps have been met, we proceed to forward the ether to the target ICO. Using the call() function and sending the pool balance along will trigger the ICO’s fallback function.

Once thing the admin (and contributors) should do before investing in the pool is making sure the pool will indeed be able to invest in the target ICO. This contract assumes the ICO has a fallback function that triggers the contract’s own buyTokens (or whatever it is called) function.

If the target ICO doesn’t have a fallback function implemented or you feel insecure about using the call() function to execute potentially insecure code, then you could use another method I’ve left commented in the code. You could instantiate the target ICO contract and call it’s buyTokens function directly.

This function also sets the investedInICO flag so we can only call this function once. If the people involved wanted to pool more money and invest a second time, they would have to create a new ICOPool contract.

Withdrawing the tokens

Once the previous step has been completed, the target ICO’s token should be holding our ICOPool’s token balance.

You should be able to check this by calling the balanceOf() function of the token by passing the ICOPool’s address to it.

So, there’s one more step left that each contributor has to complete; the withdrawal of the tokens each one is entitled to.

function withdrawTokens(address _tokenAddress) public {require(contributorsBalance[msg.sender] > 0);

    ERC20 token = ERC20(\_tokenAddress);  
    require (token.balanceOf(this) > 0);  
      
    // tokenBalance is always the max tokens the pool bought (balanceOf + already withdrawn)  
    uint tokenBalance = token.balanceOf(this).add(tokensWithdrawn);  
      
    // Get contributor share based on his contribution vs total pool  
    // poolBalance (total wei pooled) -> contributorsBalance\[msg.sender\] (wei put by msg.sender)  
    // tokenBalance (total tokens bought with poolBalance) -> tokensToWithdraw (how many tokens corresponds to msg.sender)  
    uint tokensToWithdraw = tokenBalance.mul(contributorsBalance\[msg.sender\]).div(poolBalance);  
    tokensToWithdraw = tokensToWithdraw.sub(tokensWithdrawnByContributor\[msg.sender\]);  
      
    require(tokensToWithdraw > 0);  
      
    // Keep track of tokens already withdrawn  
    tokensWithdrawn = tokensWithdrawn.add(tokensToWithdraw);  
    tokensWithdrawnByContributor\[msg.sender\] = tokensWithdrawnByContributor\[msg.sender\].add(tokensToWithdraw);  
      
    // Transfer calculated tokens to msg.sender  
    require(token.transfer(msg.sender,tokensToWithdraw));  
}

As I mentioned above, the ICOPool contract is now holding the tokens it bought. Now it’s up to each contributor to withdraw their own tokens by having each one of them call the withdrawTokens(address _tokenAddress) function.

Notice the function requires one parameter, the token’s address, not to be confused by the ICO’s address. At this point, the ICO has no utility, what we need is to change the ownership of the tokens, so we need to get a hold of the token address.

When one of the contributors calls this function, it will calculate how many tokens they are entitled to based on the total money the pool received and how much this person’s contribution represents. For example, suppose the total pool balance was 100 eth, that were used to buy 100.000 tokens. If I invested 30 ether, then I would be entitled to 30.000 tokens.

The function will figure out how many tokens of the total that were bought correspond to the contributor and then call the transfer function on the token to make the transfer from the contract to the contributor.

If you call balanceOf() again, the contributor should now own those many tokens and the contract should have those many less.

One thing to notice is that this function has been designed so that the contributor may call it more than once, and if there were more tokens than before, he would get the difference. You might be wondering why we are doing this if I previously mentioned that the ICOPool contract may only buy tokens once. Well, there’s the slight chance that our ICOPool contract might receive tokens besides buying them. There’s a popular marketing practice some ICOs are implementing lately called Airdrops where they basically send tokens to random addresses in order to promote their product.The way our withdrawTokens() function works allows the contributors to get their share of these tokens as well.

Where to go from here

There are lots of improvements that can be done to this contract. For example:

  • The person deploying the contract — the admin — will incur in more costs than the rest of the contributors because he has to pay the necessary gas to deploy the ICOPool contract, and he is not reimbursed for it. We could device some scheme that rewards more tokens to the person organizing the investment pool.
  • We could add a deadline to it so if the tokens are not bought by that date the pool gets cancelled.
  • If there was some standard or common protocol to crowdsale contracts we could make our contract friendlier to the contributors.
  • The contract could be modified to allow investing in more than one ICO, and even decide, somehow, in which ICOs to invest given their bonus schemes.

Drop me a line if you are interested in implementing this contract to invest in an ICO. And please do contact me if you find any bugs or you want to discuss any parts of this code.

I hope you enjoyed reading this article as much as I enjoyed writing it. I’m currently taking consultancy jobs related to smart contracts development. If you are planning on raising funds through an ICO or building a Blockchain-based product, feel free to get in touch with me.


Published by HackerNoon on 2017/11/24