Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERC: Fairer token crowdsale with proportional token allocation #642

Closed
hiddentao opened this issue Jun 1, 2017 · 20 comments
Closed

ERC: Fairer token crowdsale with proportional token allocation #642

hiddentao opened this issue Jun 1, 2017 · 20 comments
Labels

Comments

@hiddentao
Copy link

hiddentao commented Jun 1, 2017

Preamble

EIP: <to be assigned>
Title: Fair Token Crowdsale
Authors: Ramesh Nair (hiddentao)
Type: Informational
Category: ERC 
Status: Draft
Created: 2017-06-01
Requires: 20

Simple Summary

A contract which allows for a fairer crowdsale through proportional allocation rather than first-come-first-served.

Abstract

This ERC-20 derived token contract aims to avoid a recurring problem in popular ICO/crowdsales, whereby a small no. of participants can effectively obtain all available tokens via a combination of extremely high transaction fees (unaffordable to most participants) and "getting there first". An acute example of this problem is the recent BAT tokensale, where all tokens were allocated within 24 seconds.

Although the ICO holder's goals of raising funding are met in such situations, the fact that the majority of tokens are in the hands of relatively few players is less than ideal when considered from the perspective of decentralisation. The recurring nature of this issue has led to a race-to-be-the-fastest, causing massive network congestion due to the large volume of transactions which are created in such a short space of time.

Some solutions have been proposed, including:

  • Capping no. of tokens per address - The problem with this is that a participant could just create multiple addresses to send from. Similar potential "solutions" such as rate-limiting have similar issues associated with them.

  • Using web-based systems to authenticate the participant and their corresponding address separately - this requires more work on the part of the sale organiser. It could introduce more bugs in the process, and will thus require more testing. It also increases the work a contributor must do in order to participate (not to mention tying their identity of some sort to their contribution), which some may argue goes against the "ethos" of the system as a whole.

This ERC proposes a token contract to solve these issues without limiting how much Ether a participant can contribute. Specifically:

  • Tokens are proportionally allocated according to amount contributed. Being early or late to the game has no impact on the no. of tokens a participant receives. Every contributor will recieve some tokens, but larger contributors still get to benefit from having contributed more. Since there is no limit to contributions, any excess ETH (calculated once sale is complete) will be refunded to contributors and only the ETH required to meet the target funding level will be retained.

  • Price per token is still predictable. All that has changed is that a given participant may not get exactly as many tokens as they are hoping for since the calculation is now proportional. However, by contributing more ETH each participant can increase the proportion of the total no. of tokens number of tokens they receive, thus still incentivising contributions.

  • Sale will run the full duration. Because tokens are only allocated once the sale is complete, the sale will run its full duration, giving plenty of time for contributions to come in. No more network congestion.

  • Sale organiser and participants can finalize independently of each other. One the sale is complete, a contributor can at any time call into the contract to either obtain a full refund (if the sale failed to raise the target funding level) or claim their tokens (if the sale did raise at least the target funding level). The sale organizer can call in to ensure the raised funding gets sent to the deposit address. Once the sale is complete, neither party has to wait for the other in order to proceed with these actions.

Drawbacks of this approach:

  • The participant must call the contract twice. Previously, calling the contract during the sale would allocate the tokens. Now the participant must call the contract a second time - after the sale has ended - in order to actually "claim" their tokens and any refund due.

Example

Target ETH to raise = 100 ETH
Total supply of tokens = 100 tokens

  • Thus, price per token should be 1 ETH

Scenario 1

  • User A sends in 90 ETH
  • User B sends in 10 ETH
  • Thus, total of 100 ETH has been contributed
  • User A gets 90 tokens and no ETH refund
  • User B gets 10 tokens and no ETH refund
  • 100 ETH available to sale organiser
  • Price per token = 1 ETH

Scenario 2

  • User A sends in 190 ETH
  • User B sends in 10 ETH
  • Thus, total of 200 ETH has been contributed
  • User A gets 95 tokens and 95 ETH refund
  • User B gets 5 tokens and 5 ETH refund
  • 100 ETH available to sale organiser
  • Price per token = 1 ETH

Specification

Methods

  • function contribute() payable - Contribute ETH to the crowdsale.
  • function finalizeFunding() - Put funding into deposit address once sale is over.
  • function claimTokensAndRefund() - Claim tokens and/or get a refund once sale is done.

Events

  • event CreateFairToken(string _name) - Triggered when contract is deployed in the chain.
  • event Contribute(address _sender, uint256 _value) - Received a contribution.
  • event FinalizeSale(address _sender) - Sale finalized, funding send to deposit address.
  • event RefundContribution(address _sender, uint256 _value) - Contribution refunded.
  • event ClaimTokens(address _sender, uint256 _value) - Tokens claimed and excess ETH refunded.

Example Implementation

This uses StandardContract.sol and SafeMath.sol.

pragma solidity ^0.4.10;

import "./StandardToken.sol";
import "./SafeMath.sol";


contract FairToken is StandardToken, SafeMath {

    // metadata
    string public name = 'FairToken';
    string public symbol = 'FAIRTOKEN';
    uint256 public constant decimals = 18;
    string public version = "1.0";

    // important addresses
    address public depositAddress;      // deposit address for ETH for ICO owner

    // crowdsale params
    bool public isFinalized;            // true when ICO finalized and successful
    uint256 public targetEth;           // target ETH to raise
    uint256 public fundingStartBlock;   // when to start allowing funding
    uint256 public fundingEndBlock;     // when to stop allowing funding
    
    // events
    event CreateFairToken(string _name);
    event Contribute(address _sender, uint256 _value);
    event FinalizeSale(address _sender);
    event RefundContribution(address _sender, uint256 _value);
    event ClaimTokens(address _sender, uint256 _value);

    // calculated values
    mapping (address => uint256) contributions;    // ETH contributed per address
    uint256 contributed;      // total ETH contributed

    // constructor
    function FairToken(
        uint256 _totalSupply,
        uint256 _minEth,
        address _depositAddress,
        uint256 _fundingStartBlock,
        uint256 _fundingEndBlock)
    {
        isFinalized = false;
        totalSupply = _totalSupply * 10**decimals;
        targetEth = _minEth;
        depositAddress = _depositAddress;
        fundingStartBlock = _fundingStartBlock;
        fundingEndBlock = _fundingEndBlock;
        // log
        CreateFairToken(name);
    }

    /// Accepts ETH from a contributor
    function contribute() payable external {
        if (block.number < fundingStartBlock) throw;    // not yet begun?
        if (block.number > fundingEndBlock) throw;      // already ended?
        if (msg.value == 0) throw;                  // no ETH sent in?

        // Add to contributions
        contributions[msg.sender] += msg.value;
        contributed += msg.value;
        
        // log
        Contribute(msg.sender, msg.value);  // logs contribution
    }

    /// Finalizes the funding and sends the ETH to deposit address
    function finalizeFunding() external {
        if (isFinalized) throw;                       // already succeeded?
        if (msg.sender != depositAddress) throw;      // wrong sender?
        if (block.number <= fundingEndBlock) throw;   // not yet finished?
        if (contributed < targetEth) throw;             // not enough raised?
        
        isFinalized = true;

        // send to deposit address
        if (!depositAddress.send(targetEth)) throw;
        
        // log
        FinalizeSale(msg.sender);
    }
    
    /// Allows contributors to claim their tokens and/or a refund. If funding failed then they get back all their Ether, otherwise they get back any excess Ether
    function claimTokensAndRefund() external {
        if (0 == contributions[msg.sender]) throw;    // must have previously contributed
        if (block.number < fundingEndBlock) throw;    // not yet done?
      
        // if not enough funding
        if (contributed < targetEth) {
            // refund my full contribution
            if (!msg.sender.send(contributions[msg.sender])) throw;
            RefundContribution(msg.sender, contributions[msg.sender]);
        } else {
            // calculate how many tokens I get
            balances[msg.sender] = safeMult(totalSupply, contributions[msg.sender]) / contributed;
            // refund excess ETH
            if (!msg.sender.send(contributions[msg.sender] - (safeMult(targetEth, contributions[msg.sender]) / contributed))) throw;
            ClaimTokens(msg.sender, balances[msg.sender]);
      }
      
      contributions[msg.sender] = 0;
    }
}

Initially I didn't think the price-per-token was going to be the same for every user which is why I didn't store the tokens per ETH in the contract but just the target ETH funding level instead. But the contract could be rewritten to store tokens per ETH instead and base the calculations on that.

Copyright

Copyright and related rights waived via CC0.

(Credit to u/pa7x1 for the initial idea behind this approach).

@hiddentao
Copy link
Author

I could do with help on the calculations, particularly the divisions, since I'm not an expert and I know that floating point isn't really supported.

@joelcan
Copy link

joelcan commented Jun 1, 2017

Would it be possible to submit a refund address at the time you make your contribution? Or, would that make things too unwieldy/expensive?

@plutoegg
Copy link

plutoegg commented Jun 1, 2017

The other drawback of this approach is that it increases the concentration of funds until the refund is issued, and so increases incentive to attack any vulnerabilities of the contract, but given the typical sizes of these crowdsales the incentive is high already.

However in my view it is essential and no reason for all sales not to use this or something similar once battle tested.

@pmbauer
Copy link

pmbauer commented Jun 1, 2017

Instead of refunding 100%, burn a percentage of the returned funds to discourage monopoly-sized bids. In Scenario 2, A monopolizes the supply, but their risk exposure is capped by the ICO; they put in a big bid, own 95% of tokens with half their bid back.

Burning a percentage of the refund proportional to their percent ICO ownership would only encourage a large volume of small bids, indicating a fixed burn rate might be apropos?

@taoeffect
Copy link

taoeffect commented Jun 1, 2017

Instead of refunding 100%, burn a percentage of the returned funds to discourage monopoly-sized bids. In Scenario 2, A monopolizes the supply, but their risk exposure is capped by the ICO; they put in a big bid, own 95% of tokens with half their bid back.

Burning a percentage of the refund proportional to their percent ICO ownership would only encourage a large volume of small bids, indicating a fixed burn rate might be apropos?

+1

Let's:

  • Bring the community back down to reality so that regulatory bodies can chill easy
  • The network does not get congested like crazy
  • The fees stay low
  • Curb Your Enthusiasm™
  • What @pmbauer said
  • You don't need a gagillion dollars/btc/eth to do your project, and you're gonna self-sabotage if you think you do. Donate those extra ETH to charity or non-profits instead.

EDIT to add:

  • We don't have another "The DAO" situation

EDIT again to add:

  • Burning the excess tokens = turning greed into a positive by enriching the entire community

@taoeffect
Copy link

For email-only readers, edited my previous comment to add:

  • We don't have another "The DAO" situation
  • Burning the excess tokens = turning greed into a positive by enriching the entire community

@hiddentao
Copy link
Author

@joelcan That could be possible, though what additional advantage does that give?

@plutoegg Point noted, though additional measures to deal with that will ironically increase code complexity again.

@pmbauer @taoeffect I like the idea of penalizing excess contributions in some way, though if we opt for burning then even small contributors will lose their ETH, which one may argue unfairly penalizes them. Of course, perhaps a minimum threshold can be set, i.e. only if you contribute over a certain level of ETH will some of your excess get burned.

@hiddentao
Copy link
Author

hiddentao commented Jun 2, 2017

Another thought I had was to implement logarithmic scaling of contributions. For example:

1 eth = 1 token, for example.
9 eth = 1 token
10 eth = 1 token
11 eth = (10/10) + 1 = 2 tokens
21 eth = (20/10) + 1 = 3 tokens
99 eth = (90/10) + 1 = 10 tokens
154 eth = (100/100) + (50/10) + 4 = 10 tokens
5234 eth = (5 + 2 + 3 + 1) = 11 tokens

The idea would be to lessen the impact of larger contributions.

  • Price per token = 1 ETH
  • User A sends in 999999 ETH -> counted as 9 * 6 = 54
  • User B sends in 1 ETH -> counted as 1
  • Thus, total of 54 + 1 = 55 has been contributed
  • User A gets (54 / 55 * 100) = 98.1%, thus 98.1 tokens
  • User B gets (1 / 55 * 100) = 1.8%, thus 1.8 tokens
  • 100 ETH available to sale organiser

The whale still wins big, but the impact has been lessened. In the non-log contract above the whale would get 99.999%, leaving the small buyer with virtually nothing.

Edit: one concern is the cost of doing the log-scaling calculations.

@tawaren
Copy link

tawaren commented Jun 7, 2017

@hiddentao The logarithmic scaling does not work because contributors could again create multiple addresses and contribute a small amount with each and thus get more tokens as if when they would contribute all at once. Same goes for the threshold burning approach (multiple addresses all contributing under threshold)

@Arachnid
Copy link
Contributor

Arachnid commented Jun 7, 2017

Is this EIP material? EIPs are relevant for standardising things that multiple implementations need to interoperate with, which doesn't seem the case here. I'd highly recommend creating a repo for this, with implementation, tests, and discussion there.

@hiddentao
Copy link
Author

@tawaren The idea is that it's far more expensive for a contributor to put in large amounts. Without a way of knowing who actually owns a given address it's not possible to have a scheme where each person/entity is restricted to a certain amount of tokens.

@Arachnid Agreed, I've actually made a start over at https://github.com/hiddentao/ethereum-token-sales.

@chrisfranko
Copy link

this might be fair but fair isnt what drives the tokensale frenzy

@JosefJ
Copy link

JosefJ commented Aug 18, 2017

@hiddentao
I'm currently working on something similar with stretch-goals. The leftover (above certain amount) gets send to charity address (hard coded).

Ad calculations:
I'm currently using this:

function toPercentage (uint256 total, uint256 part) internal returns (uint256) {
        return (part*100)/total;
    }

    function fromPercentage(uint256 value, uint256 percentage) internal returns (uint256) {
        return (value*percentage)/100;
    }

100 as dividing on full percent seems fair to me, but you could go event further by just adding more zeros to both functions.

I'm using two functions instead of just one because I'm recalculating the rate in USD (using Oraclize) instead of ethers as it seems to be easier for participants to read.

@gaitchs
Copy link

gaitchs commented Dec 6, 2017

can u merge it to one file so i can launch in mist wallet ?

@hiddentao
Copy link
Author

@gaitchs That's pretty easy to do, just replace the following lines:

import "./StandardToken.sol";
import "./SafeMath.sol";

With the actual contents of those files.

@AnthonyAkentiev
Copy link

Is there any known ICO that used/is using this approach?

@gaitchs
Copy link

gaitchs commented Jan 7, 2018 via email

@AnthonyAkentiev
Copy link

AnthonyAkentiev commented Jan 7, 2018

@gaitchs I mean that is there any project already that wants to use this approach (that is of course still "in development")?

I am categorising ICO models in this doc -https://docs.google.com/document/d/1hnMjwaaYUZGch-rprvAtqay9e_ivePCpezBY5ywrrKE/edit?usp=sharing

I've already added the "Proportional Refund Model" to it.

@hiddentao Do you want me to mention you in my doc?
Btw, you can help me and add your text/comments to the document!
Your feedback will be highly appreciated. Thx

@github-actions
Copy link

github-actions bot commented Jan 2, 2022

There has been no activity on this issue for two months. It will be closed in a week if no further activity occurs. If you would like to move this EIP forward, please respond to any outstanding feedback or add a comment indicating that you have addressed all required feedback and are ready for a review.

@github-actions github-actions bot added the stale label Jan 2, 2022
@github-actions
Copy link

This issue was closed due to inactivity. If you are still pursuing it, feel free to reopen it and respond to any feedback or request a review in a comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests