mixbytes-solidity
Version:
Generic solidity smart-contracts
550 lines (438 loc) • 27.4 kB
JavaScript
/*
* Universal test: crowdsale.
*/
'use strict';
import {expectThrow} from 'openzeppelin-solidity/test/helpers/expectThrow';
import {l} from '../helpers/debug';
import '../helpers/typeExt';
function getRoles(accounts) {
return {
owner3: accounts[0],
owner1: accounts[1],
owner2: accounts[2],
investor1: accounts[2],
investor2: accounts[3],
investor3: accounts[4],
nobody: accounts[5]
};
}
export function crowdsaleUTest(accounts, instantiate, settings) {
const role = getRoles(accounts);
// default settings
const defaultSettings = {
// if true gathered ether goes to contract FundsRegistry, otherwise to pre-existed account
usingFund: false,
// Crowdsale method to be called when want to withdraw payments from funds
usingFundCrowdsalewithdrawPaymentsMethod: 'withdrawPayments',
// funds collected during previous sales and taken into consideration when computing caps
preCollectedFunds: 0,
softCap: undefined,
hardCap: undefined,
// extraPaymentFunction: '',
// rate: ,
startTime: undefined,
endTime: undefined,
maxTimeBonus: 0,
tokenTransfersDuringSale: false,
firstPostICOTxFinishesSale: true,
postICOTxThrows: true,
hasAnalytics: false,
analyticsPaymentBonus: 0
};
for (let k in defaultSettings)
if (!(k in settings))
settings[k] = defaultSettings[k];
// utility consts
const usingFund = settings.usingFund;
const AnalyticProxy = settings.hasAnalytics ? artifacts.require("AnalyticProxy") : undefined;
// utility functions
function assertBigNumberEqual(actual, expected, message=undefined) {
assert(actual.eq(expected), "{2}expected {0}, but got: {1}".format(expected, actual,
message ? message + ': ' : ''));
}
// gets balance of account/contract which store funds
async function getFundsBalance(funds) {
return await web3.eth.getBalance(usingFund ? funds.address : funds);
}
async function assertBalances(sale, token, cash, cashInitial, cashAdded) {
assert.equal(await web3.eth.getBalance(sale.address), 0, "expecting balance of the sale to be empty");
assert.equal(await web3.eth.getBalance(token.address), 0, "expecting balance of the token to be empty");
const actualCashAdded = (await getFundsBalance(cash)).sub(cashInitial);
assert(actualCashAdded.eq(cashAdded), "expecting invested cash to be {0}, but got: {1}".format(cashAdded, actualCashAdded));
}
async function assertTokenBalances(token, expectedBalances) {
for (const acc in expectedBalances) {
const balance = await token.balanceOf(acc);
assert(balance.eq(expectedBalances[acc]),
"expecting token balance of {0} to be {1}, but got: {2}".format(acc, expectedBalances[acc], balance));
}
}
// exec tx on multiowned contract
// @param args tx arguments
// @return promise
async function runMultiSigTx(contract, fn, args) {
const owners = await contract.getOwners();
let i = await contract.m_multiOwnedRequired() - 1;
for (; i; i--)
// first signatures
await fn(... args.concat({from: owners[i]}));
// the last signature
return fn(... args.concat({from: owners[i]}));
}
function calcTokens(wei, rate, bonuses) {
const base = new web3.BigNumber(wei).mul(rate);
const bonusesSum = bonuses.reduce((accumulator, currentValue) => accumulator + currentValue);
return base.mul(100 + bonusesSum).div(100);
}
// asserting that collected ether cant be transferred to owners
async function checkNotSendingEther(funds) {
await expectThrow(funds.sendEther(role.nobody, web3.toWei(20, 'finney'), {from: role.nobody}));
await expectThrow(funds.sendEther(role.investor3, web3.toWei(20, 'finney'), {from: role.investor3}));
await expectThrow(runMultiSigTx(funds, funds.sendEther, [role.owner1, web3.toWei(20, 'finney')]));
}
// asserting that collected ether cant be withdrawn by investors
async function checkNotWithdrawing(crowdsale) {
for (const from_ of [role.nobody, role.owner1, role.investor1, role.investor2, role.investor3]) {
await expectThrow(
crowdsale[settings.usingFundCrowdsalewithdrawPaymentsMethod]({from: from_})
);
}
}
// asserting that investments cant be made
async function checkNotInvesting(crowdsale, token, funds) {
const cashInitial = await getFundsBalance(funds);
for (const from_ of [role.nobody, role.owner1, role.investor1, role.investor2, role.investor3]) {
const startingBalance = await token.balanceOf(from_);
const tx = crowdsale.sendTransaction({from: from_, value: web3.toWei(20, 'finney')});
if (settings.postICOTxThrows)
await expectThrow(tx);
else
await tx;
assertBigNumberEqual(await token.balanceOf(from_), startingBalance);
}
await assertBalances(crowdsale, token, funds, cashInitial, 0);
}
// asserting that token is not circulating yet
async function checkNoTransfers(token) {
await expectThrow(token.transfer(role.nobody, 1000, {from: role.nobody}));
await expectThrow(token.transfer(role.investor3, 1000, {from: role.nobody}));
// TODO transfer (await token.balanceOf(role.investor1)).div(10).add(1000) tokens
await expectThrow(token.transfer(role.nobody, 1000, {from: role.investor1}));
await expectThrow(token.transfer(role.investor3, 1000, {from: role.investor2}));
}
// testing-related functions
async function ourInstantiate() {
const [sale, token, funds] = await instantiate(role);
// funds is either external account or FundsRegistry
if (settings.hasAnalytics) {
await sale.createMorePaymentChannels(5, {from: role.owner1});
sale.testPaymentChannels = await sale.readPaymentChannels();
sale.testNextPaymentChannel = 0;
}
return [sale, token, funds];
}
async function runFirstPostSaleTx(txPromise) {
if (settings.firstPostICOTxFinishesSale || !settings.postICOTxThrows)
// expecting first post-sale tx to succeed
await txPromise;
else
await expectThrow(txPromise);
}
const paymentFunctions = [];
paymentFunctions.push(['ff', (crowdsale, web3args) => crowdsale.sendTransaction(web3args)]);
if (settings.extraPaymentFunction)
paymentFunctions.push([settings.extraPaymentFunction, (crowdsale, web3args) => crowdsale[settings.extraPaymentFunction](web3args)]);
if (settings.hasAnalytics)
paymentFunctions.push(['analytics proxy', function(crowdsale, web3args){
const address = crowdsale.testPaymentChannels[crowdsale.testNextPaymentChannel++];
if (crowdsale.testNextPaymentChannel == crowdsale.testPaymentChannels.length)
crowdsale.testNextPaymentChannel = 0;
return AnalyticProxy.at(address).sendTransaction(web3args);
}]);
// tests
const tests = [];
tests.push(["test instantiation", async function() {
const [sale, token, cash] = await ourInstantiate();
assert.equal(await token.m_controller(), sale.address);
await assertBalances(sale, token, cash, usingFund ? 0 : await web3.eth.getBalance(cash), 0);
}]);
if (paymentFunctions.length > 1) {
tests.push(["test mixing payment functions", async function() {
const [crowdsale, token, funds] = await ourInstantiate();
const cashInitial = await getFundsBalance(funds);
const expectedTokenBalances = {};
if (settings.startTime)
await crowdsale.setTime(settings.startTime + 1, {from: role.owner1});
let tokens = new web3.BigNumber(0);
for (const [paymentFunctionName, pay] of paymentFunctions) {
await pay(crowdsale, {from: role.investor1, value: web3.toWei(20, 'finney')});
const paymentBonus = 'analytics proxy' == paymentFunctionName ? settings.analyticsPaymentBonus : 0;
tokens = tokens.add(calcTokens(web3.toWei(20, 'finney'), settings.rate,
[settings.maxTimeBonus, paymentBonus]));
}
await assertBalances(crowdsale, token, funds, cashInitial, web3.toWei(20 * paymentFunctions.length, 'finney'));
expectedTokenBalances[role.investor1] = tokens;
await assertTokenBalances(token, expectedTokenBalances);
}]);
}
for (const [paymentFunctionName, pay] of paymentFunctions) {
const paymentBonus = 'analytics proxy' == paymentFunctionName ? settings.analyticsPaymentBonus : 0;
function testName(name) {
return "{0} (payment function: {1})".format(name, paymentFunctionName);
}
tests.push([testName("test investments"), async function() {
const [crowdsale, token, funds] = await ourInstantiate();
const cashInitial = await getFundsBalance(funds);
const expectedTokenBalances = {};
if (settings.startTime) {
// too early!
await crowdsale.setTime(settings.startTime - 86400*365*2, {from: role.owner1});
await expectThrow(pay(crowdsale, {from: role.investor1, value: web3.toWei(20, 'finney')}));
await crowdsale.setTime(settings.startTime - 1, {from: role.owner1});
await expectThrow(pay(crowdsale, {from: role.investor1, value: web3.toWei(20, 'finney')}));
}
// first investment at the first second
if (settings.startTime)
await crowdsale.setTime(settings.startTime, {from: role.owner1});
await pay(crowdsale, {from: role.investor1, value: web3.toWei(20, 'finney')});
await assertBalances(crowdsale, token, funds, cashInitial, web3.toWei(20, 'finney'));
expectedTokenBalances[role.investor1] = calcTokens(web3.toWei(20, 'finney'), settings.rate,
[settings.maxTimeBonus, paymentBonus]);
await assertTokenBalances(token, expectedTokenBalances);
await expectThrow(pay(crowdsale, {from: role.nobody, value: web3.toWei(0, 'finney')}));
assert.equal(await token.balanceOf(role.nobody), 0);
// cant invest into other contracts
await expectThrow(token.sendTransaction({from: role.investor1, value: web3.toWei(20, 'finney')}));
if (usingFund)
await expectThrow(funds.sendTransaction({from: role.investor1, value: web3.toWei(20, 'finney')}));
// first investment of investor2
if (settings.startTime)
await crowdsale.setTime(settings.startTime + 100, {from: role.owner1});
await pay(crowdsale, {from: role.investor2, value: web3.toWei(100, 'finney')});
await assertBalances(crowdsale, token, funds, cashInitial, web3.toWei(120, 'finney'));
expectedTokenBalances[role.investor2] = calcTokens(web3.toWei(100, 'finney'), settings.rate,
[settings.maxTimeBonus, paymentBonus]);
await assertTokenBalances(token, expectedTokenBalances);
/*
* Final investment of this test.
* Care should be taken to reach soft cap and to know if hard cap was also reached and act accordingly.
*/
if (settings.softCap && settings.hardCap)
assert(new web3.BigNumber(settings.hardCap).gte(settings.softCap));
let currentlyCollected = new web3.BigNumber(web3.toWei(120, 'finney'));
let finalInvestment = settings.softCap && currentlyCollected.lt(settings.softCap) ?
new web3.BigNumber(settings.softCap).sub(currentlyCollected) : new web3.BigNumber(web3.toWei(30, 'finney'));
let change = new web3.BigNumber(0);
if (settings.hardCap && currentlyCollected.add(finalInvestment).gt(settings.hardCap))
change = currentlyCollected.add(finalInvestment).sub(settings.hardCap);
const hardCapTriggered = settings.hardCap && currentlyCollected.add(finalInvestment).gte(settings.hardCap);
// 2nd investment of investor1
await pay(crowdsale, {from: role.investor1, value: finalInvestment, gasPrice: 0});
currentlyCollected = currentlyCollected.add(finalInvestment);
if (hardCapTriggered)
currentlyCollected = new web3.BigNumber(settings.hardCap);
await assertBalances(crowdsale, token, funds, cashInitial, currentlyCollected);
expectedTokenBalances[role.investor1] = expectedTokenBalances[role.investor1].add(
calcTokens(finalInvestment.sub(change), settings.rate,
[settings.maxTimeBonus, paymentBonus]));
await assertTokenBalances(token, expectedTokenBalances);
if (! settings.tokenTransfersDuringSale)
await checkNoTransfers(token);
if (usingFund)
await checkNotWithdrawing(crowdsale);
if (!hardCapTriggered) {
if (! settings.tokenTransfersDuringSale)
await checkNoTransfers(token);
if (usingFund)
await checkNotSendingEther(funds);
}
// If hard cap is not triggered yet, testing late transaction
if (settings.endTime && !hardCapTriggered) {
// too late
await crowdsale.setTime(settings.endTime, {from: role.owner1});
await runFirstPostSaleTx(pay(crowdsale, {from: role.investor2, value: web3.toWei(20, 'finney')}));
await assertBalances(crowdsale, token, funds, cashInitial, currentlyCollected);
await assertTokenBalances(token, expectedTokenBalances); // anyway, nothing gained
}
// If hard cap is triggered, sale is finished
if (hardCapTriggered) {
const txPromise = pay(crowdsale, {from: role.investor2, value: web3.toWei(20, 'finney')});
if (!settings.postICOTxThrows)
await txPromise;
else
await expectThrow(txPromise);
await assertBalances(crowdsale, token, funds, cashInitial, currentlyCollected);
await assertTokenBalances(token, expectedTokenBalances); // anyway, nothing gained
}
if (settings.endTime || hardCapTriggered) {
await checkNotInvesting(crowdsale, token, funds);
}
const totalSupply = await token.totalSupply();
const totalSupplyExpected = Object.values(expectedTokenBalances).reduce((accumulator, currentValue) => accumulator.add(currentValue));
assertBigNumberEqual(totalSupply, totalSupplyExpected);
if (usingFund) {
assert.equal(await funds.getInvestorsCount(), 2);
assert.equal(await funds.m_investors(0), role.investor1);
assert.equal(await funds.m_investors(1), role.investor2);
}
}]);
if (settings.hardCap)
tests.push([testName("test hard cap"), async function() {
const [crowdsale, token, funds] = await ourInstantiate();
const cashInitial = await getFundsBalance(funds);
const expectedTokenBalances = {};
if (settings.startTime)
await crowdsale.setTime(settings.startTime, {from: role.owner1});
await pay(crowdsale, {from: role.investor1, value: web3.toWei(20, 'finney')});
await assertBalances(crowdsale, token, funds, cashInitial, web3.toWei(20, 'finney'));
expectedTokenBalances[role.investor1] = calcTokens(web3.toWei(20, 'finney'), settings.rate,
[settings.maxTimeBonus, paymentBonus]);
await assertTokenBalances(token, expectedTokenBalances);
const investor3initial = await web3.eth.getBalance(role.investor3);
await pay(crowdsale, {
from: role.investor3,
value: parseInt(settings.hardCap) + parseInt(web3.toWei(2000, 'finney')),
gasPrice: 0
});
const investor3spent = investor3initial.sub(await web3.eth.getBalance(role.investor3));
const expectedPayment = new web3.BigNumber(settings.hardCap).sub(web3.toWei(20, 'finney')).sub(settings.preCollectedFunds);
assertBigNumberEqual(investor3spent, expectedPayment, 'change has to be sent');
// optional assert.equal(await crowdsale.m_state(), 4);
await assertBalances(crowdsale, token, funds, cashInitial,
new web3.BigNumber(settings.hardCap).sub(settings.preCollectedFunds));
expectedTokenBalances[role.investor3] = calcTokens(expectedPayment, settings.rate,
[settings.maxTimeBonus, paymentBonus]);
await assertTokenBalances(token, expectedTokenBalances);
await checkNotInvesting(crowdsale, token, funds);
if (usingFund)
await checkNotWithdrawing(crowdsale);
}]);
if (settings.softCap) {
if (!usingFund)
throw new Error('softCap makes no sense without fund which can refund payments');
if (! settings.endTime)
throw new Error('softCap makes no sense without endTime');
tests.push([testName("test soft cap"), async function() {
const [crowdsale, token, funds] = await ourInstantiate();
const cashInitial = await getFundsBalance(funds);
if (settings.startTime)
await crowdsale.setTime(settings.startTime, {from: role.owner1});
await pay(crowdsale, {from: role.investor1, value: web3.toWei(20, 'finney')});
await assertBalances(crowdsale, token, funds, cashInitial, web3.toWei(20, 'finney'));
await pay(crowdsale, {from: role.investor2, value: web3.toWei(60, 'finney')});
await assertBalances(crowdsale, token, funds, cashInitial, web3.toWei(80, 'finney'));
// time is out
await crowdsale.setTime(settings.endTime, {from: role.owner1});
await runFirstPostSaleTx(pay(crowdsale, {from: role.investor1, value: web3.toWei(100, 'finney')}));
await assertBalances(crowdsale, token, funds, cashInitial, web3.toWei(80, 'finney'));
assert(web3.toBigNumber(web3.toWei(80, 'finney')).add(settings.preCollectedFunds).lt(settings.softCap));
assert.equal(await funds.m_state(), 1);
await expectThrow(
crowdsale[settings.usingFundCrowdsalewithdrawPaymentsMethod]({from: role.investor3})
);
await expectThrow(
crowdsale[settings.usingFundCrowdsalewithdrawPaymentsMethod]({from: role.owner3})
);
await crowdsale[settings.usingFundCrowdsalewithdrawPaymentsMethod]({from: role.investor2})
await assertBalances(crowdsale, token, funds, cashInitial, web3.toWei(20, 'finney'));
await expectThrow(
crowdsale[settings.usingFundCrowdsalewithdrawPaymentsMethod]({from: role.nobody})
);
await checkNoTransfers(token);
await checkNotInvesting(crowdsale, token, funds);
await checkNotSendingEther(funds);
await crowdsale[settings.usingFundCrowdsalewithdrawPaymentsMethod]({from: role.investor1})
await assertBalances(crowdsale, token, funds, cashInitial, web3.toWei(0, 'finney'));
}]);
}
}
if (usingFund) {
if (! settings.endTime)
throw new Error('makes no sense to use fund without endTime');
tests.push(["test sending ether", async function() {
const [crowdsale, token, funds] = await ourInstantiate();
const cashInitial = await getFundsBalance(funds);
if (settings.startTime)
await crowdsale.setTime(settings.startTime, {from: role.owner1});
await crowdsale.sendTransaction({from: role.investor1, value: web3.toWei(20, 'finney')});
await assertBalances(crowdsale, token, funds, cashInitial, web3.toWei(20, 'finney'));
await crowdsale.sendTransaction({
from: role.investor2,
value: parseInt(settings.softCap) + parseInt(web3.toWei(100, 'finney'))
});
await assertBalances(crowdsale, token, funds, cashInitial, parseInt(settings.softCap) + parseInt(web3.toWei(120, 'finney')));
// time is out
await crowdsale.setTime(settings.endTime, {from: role.owner1});
await runFirstPostSaleTx(crowdsale.sendTransaction({
from: role.investor1,
value: web3.toWei(100, 'finney')
}));
await assertBalances(crowdsale, token, funds, cashInitial, parseInt(settings.softCap) + parseInt(web3.toWei(120, 'finney')));
//if (settings.softCap)
// assert(web3.toBigNumber(web3.toWei(120, 'finney')).add(settings.preCollectedFunds).gt(settings.softCap));
await checkNotInvesting(crowdsale, token, funds);
await checkNotWithdrawing(crowdsale);
const x12125_initial = web3.eth.getBalance('0x1212500000000000000000000000000000000000');
const x12126_initial = web3.eth.getBalance('0x1212600000000000000000000000000000000000');
await runMultiSigTx(funds, funds.sendEther, ['0x1212500000000000000000000000000000000000', web3.toWei(40, 'finney')]);
assertBigNumberEqual((await web3.eth.getBalance('0x1212500000000000000000000000000000000000')).sub(x12125_initial), web3.toWei(40, 'finney'));
await assertBalances(crowdsale, token, funds, cashInitial, parseInt(settings.softCap) + parseInt(web3.toWei(80, 'finney')));
await runMultiSigTx(funds, funds.sendEther, ['0x1212600000000000000000000000000000000000', web3.toWei(10, 'finney')]);
assertBigNumberEqual((await web3.eth.getBalance('0x1212600000000000000000000000000000000000')).sub(x12126_initial), web3.toWei(10, 'finney'));
await assertBalances(crowdsale, token, funds, cashInitial, parseInt(settings.softCap) + parseInt(web3.toWei(70, 'finney')));
await runMultiSigTx(funds, funds.sendEther, ['0x1212500000000000000000000000000000000000', web3.toWei(55, 'finney')]);
assertBigNumberEqual((await web3.eth.getBalance('0x1212500000000000000000000000000000000000')).sub(x12125_initial), web3.toWei(95, 'finney'));
await assertBalances(crowdsale, token, funds, cashInitial, parseInt(settings.softCap) + parseInt(web3.toWei(15, 'finney')));
await checkNotInvesting(crowdsale, token, funds);
await checkNotWithdrawing(crowdsale);
}]);
}
if (settings.hasAnalytics)
tests.push(["test payment channels", async function() {
const [crowdsale, token, funds] = await ourInstantiate();
const cashInitial = await getFundsBalance(funds);
const expectedTokenBalances = {};
await expectThrow(crowdsale.createMorePaymentChannels(5, {from: role.investor3}));
await expectThrow(crowdsale.createMorePaymentChannels(5, {from: role.nobody}));
assert.equal(await crowdsale.paymentChannelsCount(), 5);
const channel1 = await crowdsale.m_paymentChannels(0);
const channel2 = await crowdsale.m_paymentChannels(1);
const channel3 = await crowdsale.m_paymentChannels(2);
if (settings.startTime)
await crowdsale.setTime(settings.startTime + 2, {from: role.owner1});
// investor1 -> channel3
await AnalyticProxy.at(channel3).sendTransaction({from: role.investor1, value: web3.toWei(20, 'finney')});
await assertBalances(crowdsale, token, funds, cashInitial, web3.toWei(20, 'finney'));
expectedTokenBalances[role.investor1] = calcTokens(web3.toWei(20, 'finney'), settings.rate,
[settings.maxTimeBonus, settings.analyticsPaymentBonus]);
await assertTokenBalances(token, expectedTokenBalances);
assertBigNumberEqual(await crowdsale.m_investmentsByPaymentChannel(channel3), web3.toWei(20, 'finney'));
// investor2 -> channel2
await AnalyticProxy.at(channel2).sendTransaction({from: role.investor2, value: web3.toWei(100, 'finney')});
await assertBalances(crowdsale, token, funds, cashInitial, web3.toWei(120, 'finney'));
expectedTokenBalances[role.investor2] = calcTokens(web3.toWei(100, 'finney'), settings.rate,
[settings.maxTimeBonus, settings.analyticsPaymentBonus]);
await assertTokenBalances(token, expectedTokenBalances);
assertBigNumberEqual(await crowdsale.m_investmentsByPaymentChannel(channel3), web3.toWei(20, 'finney'));
assertBigNumberEqual(await crowdsale.m_investmentsByPaymentChannel(channel2), web3.toWei(100, 'finney'));
// investor3 -> channel3
await AnalyticProxy.at(channel3).sendTransaction({from: role.investor3, value: web3.toWei(30, 'finney')});
await assertBalances(crowdsale, token, funds, cashInitial, web3.toWei(150, 'finney'));
expectedTokenBalances[role.investor3] = calcTokens(web3.toWei(30, 'finney'), settings.rate,
[settings.maxTimeBonus, settings.analyticsPaymentBonus]);
await assertTokenBalances(token, expectedTokenBalances);
assertBigNumberEqual(await crowdsale.m_investmentsByPaymentChannel(channel3), web3.toWei(50, 'finney'));
assertBigNumberEqual(await crowdsale.m_investmentsByPaymentChannel(channel2), web3.toWei(100, 'finney'));
// 2nd investment of investor1 -> channel3
await AnalyticProxy.at(channel3).sendTransaction({from: role.investor1, value: web3.toWei(30, 'finney')});
await assertBalances(crowdsale, token, funds, cashInitial, web3.toWei(180, 'finney'));
expectedTokenBalances[role.investor1] = expectedTokenBalances[role.investor1].add(
calcTokens(web3.toWei(30, 'finney'), settings.rate, [settings.maxTimeBonus, settings.analyticsPaymentBonus]));
await assertTokenBalances(token, expectedTokenBalances);
assertBigNumberEqual(await crowdsale.m_investmentsByPaymentChannel(channel3), web3.toWei(80, 'finney'));
assertBigNumberEqual(await crowdsale.m_investmentsByPaymentChannel(channel2), web3.toWei(100, 'finney'));
await expectThrow(crowdsale.createMorePaymentChannels(5, {from: role.investor3}));
await expectThrow(crowdsale.createMorePaymentChannels(5, {from: role.nobody}));
}]);
return tests;
}