Skip to content

Commit

Permalink
Added milestone based pricing.
Browse files Browse the repository at this point in the history
  • Loading branch information
miohtama committed Apr 5, 2017
1 parent fc51b9e commit 102a678
Show file tree
Hide file tree
Showing 12 changed files with 325 additions and 35 deletions.
1 change: 0 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ Features and design goals

* **Auditable**: Our tool chain supports `verifiable EtherScan.io contract builds <http://ico.readthedocs.io/en/latest/verification.html>`_


* **Reusable**: The contract code is modularized and reusable across different projects, all variables are parametrized and there are no hardcoded values or magic numbers

* **Refund**: Built-in refund and minimum funding goal protect investors
Expand Down
2 changes: 1 addition & 1 deletion contracts/Crowdsale.sol
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ contract Crowdsale is Haltable {
function invest(address receiver) inState(State.Funding) stopInEmergency payable public {

uint weiAmount = msg.value;
uint tokenAmount = pricingStrategy.calculatePrice(weiAmount, weiRaised, tokensSold);
uint tokenAmount = pricingStrategy.calculatePrice(weiAmount, weiRaised, tokensSold, msg.sender);

if(tokenAmount == 0) {
// Dust transaction
Expand Down
3 changes: 2 additions & 1 deletion contracts/FlatPricing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "./PricingStrategy.sol";
*/
contract FlatPricing is PricingStrategy {

/* How many weis one token costs */
uint public tokenPrice;

function FlatPricing(uint _tokenPrice) {
Expand All @@ -18,7 +19,7 @@ contract FlatPricing is PricingStrategy {
*
* @param {uint amount} Buy-in value in wei.
*/
function calculatePrice(uint value, uint tokensSold, uint weiRaised) public constant returns (uint) {
function calculatePrice(uint value, uint tokensSold, uint weiRaised, address msgSender) public constant returns (uint) {
return value / tokenPrice;
}

Expand Down
116 changes: 116 additions & 0 deletions contracts/MilestonePricing.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
pragma solidity ^0.4.6;

import "./PricingStrategy.sol";


/**
* Time milestone based pricing with special support for pre-ico deals.
*/
contract MilestonePricing is PricingStrategy {

uint public constant MAX_MILESTONE = 10;

// This is our PresaleFundCollector contract
address public preicoContractAddress;

// Price for presale investors weis per toke
uint public preicoPrice;

/**
* Define pricing schedule using milestones.
*/
struct Milestone {

// UNIX timestamp when this milestone kicks in
uint time;

// How many tokens per satoshi you will get after this milestone has been passed
uint price;
}

// Store milestones in a fixed array, so that it can be seen in a blockchain explorer
// Milestone 0 is always (0, 0)
// (TODO: change this when we confirm dynamic arrays are explorable)
Milestone[10] public milestones;

// How many active milestones we have
uint public milestoneCount;

/**
* @param _preicoContractAddress PresaleFundCollector address
* @param _preicoPrice How many weis one token cost for pre-ico investors
* @param _milestones uint[] miletones Pairs of (time, price)
*/
function MilestonePricing(address _preicoContractAddress, uint _preicoPrice, uint[] _milestones) {

preicoContractAddress = _preicoContractAddress;
preicoPrice = _preicoPrice;

// Need to have tuples, length check
if(_milestones.length % 2 == 1 || _milestones.length >= MAX_MILESTONE) {
throw;
}

milestoneCount = _milestones.length / 2;

for(uint i=0; i<_milestones.length/2; i++) {
milestones[i].time = _milestones[i*2];
milestones[i].price = _milestones[i*2+1];
}
}

/**
* Iterate through milestones.
*
* You reach end of milestones when price = 0
*
* @return tuple (time, price)
*/
function getMilestone(uint n) public constant returns (uint, uint) {
return (milestones[n].time, milestones[n].price);
}

/**
* Get the current milestone or bail out if we are not in the milestone periods.
*
* @return {[type]} [description]
*/
function getCurrentMilestone() private constant returns (Milestone) {
uint i;
uint price;

for(i=0; i<milestones.length; i++) {
if(now < milestones[i].time) {
return milestones[i-1];
}
}
}

/**
* Get the current price.
*
* @return The current price or 0 if we are outside milestone period
*/
function getCurrentPrice() public constant returns (uint result) {
return getCurrentMilestone().price;
}

/**
* Calculate the current price for buy in amount.
*/
function calculatePrice(uint value, uint tokensSold, uint weiRaised, address msgSender) public constant returns (uint) {

// This investor is coming through pre-ico
if(msgSender == preicoContractAddress) {
return value / preicoPrice;
}

uint price = getCurrentPrice();
return value / price;
}

function() payable {
throw; // No money on this contract
}

}
2 changes: 1 addition & 1 deletion contracts/PricingStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ contract PricingStrategy {
/**
* When somebody tries to buy tokens for X eth, calculate how many tokens they get.
*/
function calculatePrice(uint value, uint tokensSold, uint weiRaised) public constant returns (uint tokenAmount);
function calculatePrice(uint value, uint tokensSold, uint weiRaised, address msgSender) public constant returns (uint tokenAmount);
}
2 changes: 2 additions & 0 deletions contracts/Wallet.sol
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pragma solidity ^0.4.8;

/**
* This is a multisig wallet based on Gav's original implementation, daily withdraw limits removed.
*
Expand Down
1 change: 1 addition & 0 deletions ico/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ def nice_list_pytest_itemcollected(item):
from ico.tests.fixtures.flatprice import * # noqa
from ico.tests.fixtures.releasable import * # noqa
from ico.tests.fixtures.finalize import * # noqa
from ico.tests.fixtures.presale import * # noqa
166 changes: 166 additions & 0 deletions ico/tests/contracts/test_milestone_pricing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""Milestone based pricing"""
import datetime

import pytest
from eth_utils import to_wei
from ethereum.tester import TransactionFailed
from web3.contract import Contract


from ico.tests.utils import time_travel


@pytest.fixture
def presale_fund_collector(chain, presale_freeze_ends_at, team_multisig) -> Contract:
"""In actual ICO, the price is doubled (for testing purposes)."""

args = [
team_multisig,
presale_freeze_ends_at,
to_wei("50", "ether"), # Minimum presale buy in is 50 ethers

]
tx = {
"from": team_multisig,
}
presale_fund_collector, hash = chain.provider.deploy_contract('PresaleFundCollector', deploy_args=args, deploy_transaction=tx)
return presale_fund_collector


@pytest.fixture
def start_time() -> int:
return int((datetime.datetime(2017, 4, 15, 16, 00) - datetime.datetime(1970, 1, 1)).total_seconds())


@pytest.fixture
def token(uncapped_token) -> int:
"""Token contract used in milestone tests"""
return uncapped_token


@pytest.fixture
def milestone_pricing(chain, presale_fund_collector, start_time):

week = 24*3600 * 7

args = [
presale_fund_collector.address,
to_wei("0.05", "ether"),
[
start_time + 0, to_wei("0.10", "ether"),
start_time + week*1, to_wei("0.12", "ether"),
start_time + week*2, to_wei("0.13", "ether"),
start_time + week*4, to_wei("0.13", "ether"),
],
]

tx = {
"gas": 4000000
}
contract, hash = chain.provider.deploy_contract('MilestonePricing', deploy_args=args, deploy_transaction=tx)
return contract


@pytest.fixture
def milestone_ico(chain, beneficiary, team_multisig, start_time, milestone_pricing, preico_cap, preico_funding_goal, token, presale_fund_collector) -> Contract:
"""Create a crowdsale contract that uses milestone based pricing."""

ends_at = start_time + 4*24*3600

args = [
token.address,
milestone_pricing.address,
team_multisig,
beneficiary,
start_time,
ends_at,
0,
]

tx = {
"from": team_multisig,
}

contract, hash = chain.provider.deploy_contract('UncappedCrowdsale', deploy_args=args, deploy_transaction=tx)

assert contract.call().owner() == team_multisig
assert not token.call().released()

# Allow pre-ico contract to do mint()
token.transact({"from": team_multisig}).setMintAgent(contract.address, True)
assert token.call().mintAgents(contract.address) == True

return contract


def test_milestone_getter(chain, milestone_pricing, start_time):
"""Milestone data is exposed to the world."""

time, price = milestone_pricing.call().getMilestone(0)
assert time == 1492272000
assert price == 100000000000000000


def test_milestone_prices(chain, milestone_pricing, start_time, customer):
"""We get correct milestone prices for different dates."""

time_travel(chain, start_time - 1)
with pytest.raises(TransactionFailed):
# Div by zero, crowdsale has not begin yet
assert milestone_pricing.call().getCurrentPrice()

time_travel(chain, start_time)
assert milestone_pricing.call().getCurrentPrice() == to_wei("0.10", "ether")

time_travel(chain, start_time + 1)
assert milestone_pricing.call().getCurrentPrice() == to_wei("0.10", "ether")

# 1 week forward
time_travel(chain, int((datetime.datetime(2017, 4, 22, 16, 0) - datetime.datetime(1970, 1, 1)).total_seconds()))
assert milestone_pricing.call().getCurrentPrice() == to_wei("0.12", "ether")

# 2 week forward
time_travel(chain, int((datetime.datetime(2017, 4, 29, 16, 0) - datetime.datetime(1970, 1, 1)).total_seconds()))
assert milestone_pricing.call().getCurrentPrice() == to_wei("0.13", "ether")

# See that we divide price correctly
assert milestone_pricing.call().calculatePrice(
to_wei("0.26", "ether"),
0,
0,
customer
) == 2


def test_milestone_calculate_preico_price(chain, milestone_pricing, start_time, presale_fund_collector):
"""Preico contributors get their special price."""

# 1 week forward
time_travel(chain, int((datetime.datetime(2017, 4, 22, 16, 0) - datetime.datetime(1970, 1, 1)).total_seconds()))

# Pre-ico address always buys at the fixed price
assert milestone_pricing.call().calculatePrice(
to_wei("0.05", "ether"),
0,
0,
presale_fund_collector.address
) == 1


def test_presale_move_to_milestone_crowdsale(chain, presale_fund_collector, milestone_ico, token, start_time, team_multisig, customer, customer_2):
"""When pre-ico contract funds are moved to the crowdsale, the pre-sale investors gets tokens with a preferred price and not the current milestone price."""

value = to_wei(50, "ether")
presale_fund_collector.transact({"from": customer, "value": value}).invest()

# ICO begins, Link presale to an actual ICO
presale_fund_collector.transact({"from": team_multisig}).setCrowdsale(milestone_ico.address)
time_travel(chain, start_time)

# Load funds to ICO
presale_fund_collector.transact().parcipateCrowdsaleAll()

# Tokens received, paid by preico price
milestone_ico.call().investedAmountOf(customer) == to_wei(50, "ether")
token.call().balanceOf(customer) == 50 / 0.050

28 changes: 0 additions & 28 deletions ico/tests/contracts/test_presale.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,6 @@
from ico.utils import get_constructor_arguments


@pytest.fixture
def presale_freeze_ends_at() -> int:
"""How long presale funds stay frozen until refund."""
return int(datetime.datetime(2017, 1, 1).timestamp())


@pytest.fixture
def presale_fund_collector(chain, presale_freeze_ends_at, team_multisig) -> Contract:
"""In actual ICO, the price is doubled (for testing purposes)."""
args = [
team_multisig,
presale_freeze_ends_at,
to_wei(1, "ether")
]
tx = {
"from": team_multisig,
}
presale_fund_collector, hash = chain.provider.deploy_contract('PresaleFundCollector', deploy_args=args, deploy_transaction=tx)
return presale_fund_collector


@pytest.fixture
def presale_crowdsale(chain, presale_fund_collector, uncapped_flatprice, team_multisig):
"""ICO associated with the presale where funds will be moved to a presale."""
presale_fund_collector.transact({"from": team_multisig}).setCrowdsale(uncapped_flatprice.address)
return uncapped_flatprice


def test_invest_presale(presale_fund_collector, customer, presale_freeze_ends_at):
"""Customer can invest into a presale."""
value = to_wei(1, "ether")
Expand Down
4 changes: 2 additions & 2 deletions ico/tests/contracts/test_two_stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def test_buy_both_stages(chain: TestRPCChain, preico: Contract, actual_ico: Cont

# First buy tokens when pre-ICO is open
first_buy = to_wei(100000, "ether")
first_batch = flat_pricing.call().calculatePrice(first_buy, 0, 0)
first_batch = flat_pricing.call().calculatePrice(first_buy, 0, 0, customer)
time_travel(chain, preico_starts_at + 1)
assert preico.call().getState() == CrowdsaleState.Funding
assert actual_ico.call().getState() == CrowdsaleState.PreFunding
Expand All @@ -144,7 +144,7 @@ def test_buy_both_stages(chain: TestRPCChain, preico: Contract, actual_ico: Cont
time_travel(chain, actual_ico_starts_at + 1)
assert actual_ico.call().getState() == CrowdsaleState.Funding
second_buy = to_wei(2, "ether")
second_batch = final_pricing.call().calculatePrice(second_buy, 0, 0)
second_batch = final_pricing.call().calculatePrice(second_buy, 0, 0, customer)
actual_ico.transact({"from": customer, "value": second_buy}).buy()

# Close the actual ICO and check tokens are transferable
Expand Down
2 changes: 1 addition & 1 deletion ico/tests/contracts/test_uncapped_flatprice.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def test_buy_reach_goal(chain: TestRPCChain, flat_pricing, uncapped_flatprice: C
wei_value = preico_funding_goal

# Check that we don't have issues with our pricing
assert flat_pricing.call().calculatePrice(wei_value, 0, 0) > 0
assert flat_pricing.call().calculatePrice(wei_value, 0, 0, customer) > 0

uncapped_flatprice.transact({"from": customer, "value": wei_value}).buy()

Expand Down

0 comments on commit 102a678

Please sign in to comment.