EOS Crowdsale Contract Source Code Inspection and Analysis - Line by LinesteemCreated with Sketch.

in #eos7 years ago

Hi Everyone

So I plan to join the red hot EOS ICO sale. You can read more about my analysis here: https://steemit.com/eos/@gaman/eos-initial-coin-offering-primer.

But before I invest, let me double check the EOS contract source code. I will be doing a favor to the community too by auditing the EOS contract source code. I have taken the source code from: https://github.com/EOSIO/eos-token-distribution/blob/master/src/eos.sol

Contract Creation

So lets start:


pragma solidity ^0.4.11;
import "ds-auth/auth.sol";
import "ds-exec/exec.sol";
import "ds-math/math.sol";
import "ds-token/token.sol";

So, the program started off stating to the compiler that this code will be for solidity version 0.4.11 and up (but below 0.5). No functions will break even if it is compiled in newer solidity versions as long as it is below version 0.5

Then, it imported the Dappsys packages (DSAuth, DSExec, DSMath and DSToken) for building smart contract.


contract EOSSale is DSAuth, DSExec, DSMath {
    DSToken  public  EOS;                  // The EOS token itself
    uint128  public  totalSupply;          // Total EOS amount created
    uint128  public  foundersAllocation;   // Amount given to founders
    string   public  foundersKey;          // Public key of founders

    uint     public  openTime;             // Time of window 0 opening
    uint     public  createFirstDay;       // Tokens sold in window 0

    uint     public  startTime;            // Time of window 1 opening
    uint     public  numberOfDays;         // Number of windows after 0
    uint     public  createPerDay;         // Tokens sold in each window

    mapping (uint => uint)                       public  dailyTotals;
    mapping (uint => mapping (address => uint))  public  userBuys;
    mapping (uint => mapping (address => bool))  public  claimed;
    mapping (address => string)                  public  keys;

    event LogBuy      (uint window, address user, uint amount);
    event LogClaim    (uint window, address user, uint amount);
    event LogRegister (address user, string key);
    event LogCollect  (uint amount);
    event LogFreeze   ();

Here, we see that the contract inherited the Dappsys building blocks. DSAuth is a package for handling owner permissions - specifically minting, starting and stopping the token. DSMath is a package for handling arithmetic operations in the smart contract. It contains the functions wad and rad to handle arithmetic operations for 18 and 28 decimal places. DSExec is used to collect the ethers that are contributed.

Then, the variables that are going to be used are declared.


    function EOSSale(
        uint     _numberOfDays,
        uint128  _totalSupply,
        uint     _openTime,
        uint     _startTime,
        uint128  _foundersAllocation,

        string   _foundersKey
    ) {
        numberOfDays       = _numberOfDays;
        totalSupply        = _totalSupply;
        openTime           = _openTime;
        startTime          = _startTime;
        foundersAllocation = _foundersAllocation;
        foundersKey        = _foundersKey;

        createFirstDay = wmul(totalSupply, 0.2 ether);
        createPerDay = div(
            sub(sub(totalSupply, foundersAllocation), createFirstDay),
            numberOfDays
        );

        assert(numberOfDays > 0);
        assert(totalSupply > foundersAllocation);
        assert(openTime < startTime);

This function is run during the contract creation phase. It only run once. The first part is initializing the variables of the token (i.e. the number of days for the token sale, total supply, open time, etc).

Next, it calculated the number of tokens for the first period and assign it to the variable: createFirstDay. Here, they used the wad multiplication (wmul) to calculate 20% of the total supply. wmul has to be used since we are calculating arithmetic functions up to 18 digits of precision and the Solidity compiler does not yet support fixed point mathematics natively. 0.2 ether is used to represent 20%, as a workaround to type float.

Then for the succeeding periods of token sale, the amount of coins to be sold is equals to:
the total supply
minus the founders allocation
minus the tokens during the first day
and then divided by the number of days of token sale.

Some checks are done to make sure the parameters are correct:

  1. the number of days of token sale must be greater than 0
  2. the total supply must be greater than the founders allocation

Contract Initialization


  function initialize(DSToken eos) auth {
        assert(address(EOS) == address(0));
        assert(eos.owner() == address(this));
        assert(eos.authority() == DSAuthority(0));
        assert(eos.totalSupply() == 0);

        EOS = eos;
        EOS.mint(totalSupply);

        // Address 0xb1 is provably non-transferrable
        EOS.push(0xb1, foundersAllocation);
        keys[0xb1] = foundersKey;
        LogRegister(0xb1, foundersKey);
    }

After the contract creation phase, the owner of the contract would then call the initialize function. If you look at this function, it has the modifier auth in the function declaration. This makes sure that only the owner of the contract ( or someone given permission to ) can call this function.

There are some checks made at the start of the code. First, it checks that the EOS variable address is 0. This means that this function has not yet been called before. Then it made sure that the owner of the token is the contract creator and that the total supply of tokens is still 0.

It then proceeds to mint the total supply of the token. Then the next few lines of code is to throw away the tokens that are allocated to the founders. This is because the EOS will be a new blockchain and the founders will be given their allocation of this new coin. They don't need the EOS token to get the new coin.

Calculating the Window Periods


    function time() constant returns (uint) {
        return block.timestamp;
    }

    function today() constant returns (uint) {
        return dayFor(time());
    }

    // Each window is 23 hours long so that end-of-window rotates
    // around the clock for all timezones.
    function dayFor(uint timestamp) constant returns (uint) {
        return timestamp < startTime
            ? 0
            : sub(timestamp, startTime) / 23 hours + 1;
    }

    function createOnDay(uint day) constant returns (uint) {
        return day == 0 ? createFirstDay : createPerDay;
    }

The time function gets an approximate of the current time with the last block's timestamp. It is then used to calculate the Nth day of the crowd sale. The first five days is window 0. After which, every 23 hours is another window. By dividing it with 23 hours rather than 24 hours in a day, this allows the starting period for each window period to be at different time zone. This makes it fair to investors in different parts of the world.

The function createOnDay will be used later to determine the number of tokens for a particular period. This will then be divided by the number of contributions during that period to calculate the amount of token each investor will get.

Contribution


   // This method provides the buyer some protections regarding which
    // day the buy order is submitted and the maximum price prior to
    // applying this payment that will be allowed.

    function buyWithLimit(uint day, uint limit) payable {
        assert(time() >= openTime && today() <= numberOfDays);
        assert(msg.value >= 0.01 ether);
        assert(day >= today());
        assert(day <= numberOfDays);

        userBuys[day][msg.sender] += msg.value;
        dailyTotals[day] += msg.value;

        if (limit != 0) {
            assert(dailyTotals[day] <= limit);
        }

        LogBuy(day, msg.sender, msg.value);
    }

    function buy() payable {
       buyWithLimit(today(), 0);
    }

    function () payable {
       buy();
    }


The function without name is the default function that will be called when the contract receives ether. It then calls the function buy() which then calls the function buyWithlimit.

Some checks are made at the start of the function of buyWithLimit. It checks that it is still within the crowd sale days and you are contributing more than the minimum amount of 0.01 ether.

Then, it records your contribution and added it to the daily total of contributions. Since there is no limit (hard cap), limit is set here to 0.

Claiming


   function claim(uint day) {
        assert(today() > day);
        if (claimed[day][msg.sender] || dailyTotals[day] == 0) {
            return;
        }

        // This will have small rounding errors, but the token is
        // going to be truncated to 8 decimal places or less anyway
        // when launched on its own chain.

        var dailyTotal = cast(dailyTotals[day]);
        var userTotal  = cast(userBuys[day][msg.sender]);
        var price      = wdiv(cast(createOnDay(day)), dailyTotal);
        var reward     = wmul(price, userTotal);

        claimed[day][msg.sender] = true;
        EOS.push(msg.sender, reward);

        LogClaim(day, msg.sender, reward);
    }

    function claimAll() {
        for (uint i = 0; i < today(); i++) {
            claim(i);
        }
    }

You won't be given any token during each window period. You can only claim your tokens when the window period ended. The first assert in the claim function is to make sure you are claiming for a window period that has already ended.

The next is to check that you have not yet claimed for the period that you are claiming and that the total invested for that period is greater than 0 eth.

Then, it calculates the amount of token that you will receive per 1 ether invested for that window period. It then multiplies this by the amount of ether you contributed. It then transfers those tokens to you.

A claimAll function is provided for investors who invested in multiple window period. But how much gas would be needed to perform this if you have invested in 341 periods? Hmmm....


    // Value should be a public key.  Read full key import policy.
    // Manually registering requires a base58
    // encoded using the STEEM, BTS, or EOS public key format.

    function register(string key) {
        assert(today() <=  numberOfDays + 1);
        assert(bytes(key).length <= 64);

        keys[msg.sender] = key;
        LogRegister(msg.sender, key);
    }

The register function maps an EOS public key to your Ethereum public key. This is one of the conditions of the ICO. If at the end of the contribution period and you didn't register, you won't receive your coin in the EOS blockchain when it launches. Basically this allows an automated way of giving you your EOS coins in the new EOS blockchain.

Collecting the Money


    // Crowdsale owners can collect ETH any number of times

    function collect() auth {
        assert(today() > 0); // Prevent recycling during window 0
        exec(msg.sender, this.balance);
        LogCollect(this.balance);
    }

    // Anyone can freeze the token 1 day after the sale ends
    function freeze() {
        assert(today() > numberOfDays + 1);
        EOS.stop();
        LogFreeze();
    }


collect functions allows the crowd sale owners to collect the Eth in the contract account any number of times. The freeze function freezes the crowd sale and can only be done when all the window periods are completed.

Conclusion

I didn't find any errors on the EOS contract. The only concern I have is the gas limit to be used in executing the claimAll command. I guess, the investor just have to provide a sufficiently high amount to ensure that there is enough gas to complete the transaction.

Sort:  

Thank you for taking the time out to do this. Was really helpful . Will be referencing this post in the future.

so ETH really are not locked until end of ICO, thanks for clarifying

It is very sad that this post does not have a valuation of minimum 100 usd. Really excellent work, I wish I had more reputation to give a greater reward. Vote up and resteem

Thank you very much! :) I will be doing more inspection work.