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.
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! 📄
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.
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:
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:
Notice there’s a complex relationship between all these parameters so we don’t risk the chance of the investment pool getting stuck:
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.
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.
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.
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.
There are lots of improvements that can be done to this contract. For example:
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.