tokyo-solidity-template
Version:
Make solidity contract based on user input
566 lines (434 loc) • 21.5 kB
JavaScript
import moment from "moment";
import get from "lodash/get";
import range from "lodash/range";
import schema from "tokyo-schema";
import ether from "./helpers/ether";
import { advanceBlock, advanceManyBlock } from "./helpers/advanceToBlock";
import increaseTime, { increaseTimeTo, duration } from "./helpers/increaseTime";
import latestTime from "./helpers/latestTime";
import EVMThrow from "./helpers/EVMThrow";
import { capture, restore, Snapshot } from "./helpers/snapshot";
import timer from "./helpers/timer";
import sendTransaction from "./helpers/sendTransaction";
import "./helpers/upgradeBigNumber";
const BigNumber = web3.BigNumber;
const eth = web3.eth;
const should = require("chai")
.use(require("chai-as-promised"))
.use(require("chai-bignumber")(BigNumber))
.should();
const KYC = artifacts.require("./KYC.sol");
const Vault = artifacts.require("./MultiHolderVault.sol");
const Locker = artifacts.require("./Locker.sol");
const Token = artifacts.require("./RankingBallGoldToken.sol");
const Crowdsale = artifacts.require("./RankingBallGoldCrowdsale.sol");
contract("RankingBallGoldCrowdsale", async ([ owner, other, investor1, investor2, investor3, ...accounts ]) => {
// contract instances
let kyc, vault, locker, token, crowdsale;
// TX parameteres
const gas = 2000000;
// test parameteres
const input = schema.validate(getInput()).value;
const timeBonuses = input.sale.rate.bonus
.time_bonuses.map(b => b.bonus_time_ratio); // 20%, 15%, 10%, 5%
const baseRate = new BigNumber(input.sale.rate.base_rate);
const coeff = new BigNumber(input.sale.coeff); // 10000
const maxCap = new BigNumber(input.sale.max_cap); // 40000 ether
const minCap = new BigNumber(input.sale.min_cap); // 5000 ether
before(async () => {
// load contracts
kyc = await KYC.deployed();
vault = await Vault.deployed();
locker = await Locker.deployed();
token = await Token.deployed();
crowdsale = await Crowdsale.deployed();
console.log(`
kyc: ${ kyc.address }
vault: ${ vault.address }
locker: ${ locker.address }
token: ${ token.address }
crowdsale: ${ crowdsale.address }
`);
});
// const snapshot = new Snapshot();
// before(snapshot.captureContracts);
// after(snapshot.restoreContracts);
// helper functions
const advanceBlocks = () => advanceManyBlock(input.sale.valid_purchase.block_interval);
it("setup contracts", async () => {
await kyc.register(investor1, { from: owner })
.should.be.fulfilled;
await kyc.register(investor2, { from: owner })
.should.be.fulfilled;
await kyc.register(investor3, { from: owner })
.should.be.fulfilled;
await advanceBlocks();
});
it("check parameters", async () => {
(await crowdsale.nextTokenOwner())
.should.be.equal(input.sale.new_token_owner);
(await crowdsale.cap())
.should.be.bignumber.equal(maxCap);
maxCap.should.be.bignumber.equal(ether(40000));
(await crowdsale.goal())
.should.be.bignumber.equal(minCap);
});
describe("Before start time", async () => {
it("reject buying tokens", async () => {
const investAmount = ether(10);
await sendTransaction({
from: investor1, to: crowdsale.address, value: investAmount, gas,
}).should.be.rejectedWith(EVMThrow);
});
});
describe("After start time (time bonus 20%)", async () => {
const targetTime = input.sale.start_time;
const scenarioIndex = 1;
const timeBonusIndex = 0;
const timeBonus = timeBonuses[ timeBonusIndex ];
it(`increase time to ${ targetTime }`, async () => {
await increaseTimeTo(targetTime)
.should.be.fulfilled;
});
it("reject buying tokens for unknown account", async () => {
const investAmount = ether(10);
await sendTransaction({
from: other, to: crowdsale.address, value: investAmount, gas,
}).should.be.rejectedWith(EVMThrow);
});
it("accept buying tokens for valid account", async () => {
const investor = investor1;
const investAmount = ether(10); // 39990 ether remains
const rate = calcRate(baseRate, timeBonus, coeff);
const tokenAmount = investAmount.mul(rate);
await advanceBlocks();
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.fulfilled;
(await token.balanceOf(investor))
.should.be.bignumber.equal(tokenAmount);
});
it("reject buying tokens within a few blocks", async () => {
const investor = investor1;
const investAmount = ether(10);
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.rejectedWith(EVMThrow);
});
describe(`Over max cap scenario #${ scenarioIndex }`, async () => {
const snapshot2 = new Snapshot();
before(snapshot2.captureContracts);
after(snapshot2.restoreContracts);
it("accept buying tokens over max cap", async () => {
const investor = investor1;
const investAmount = maxCap;
const fundedAmount = maxCap.sub(ether(10));
const rate = calcRate(baseRate, timeBonus, coeff);
const tokenAmount = fundedAmount.mul(rate);
const tokenBalance = await token.balanceOf(investor);
await advanceBlocks();
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.fulfilled;
(await token.balanceOf(investor))
.should.be.bignumber.equal(tokenBalance.add(tokenAmount));
});
it("should finalize crowdsale", async () => {
await crowdsale.finalize()
.should.be.fulfilled;
});
});
});
describe("After start time (time bonus 15%)", async () => {
const scenarioIndex = 2;
const timeBonusIndex = 1;
const timeBonus = timeBonuses[ timeBonusIndex ];
const targetTime = input.sale.rate.bonus
.time_bonuses[ timeBonusIndex - 1 ].bonus_time_stage + 1;
it(`increase time to ${ targetTime }`, async () => {
await increaseTimeTo(targetTime)
.should.be.fulfilled;
});
it("reject buying tokens for unknown account", async () => {
const investAmount = ether(10);
await sendTransaction({
from: other, to: crowdsale.address, value: investAmount, gas,
}).should.be.rejectedWith(EVMThrow);
});
it("accept buying tokens for valid account", async () => {
const investor = investor3;
const investAmount = ether(100); // 39890 ether remains
const rate = calcRate(baseRate, timeBonus, coeff);
const tokenAmount = investAmount.mul(rate);
await advanceBlocks();
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.fulfilled;
(await token.balanceOf(investor))
.should.be.bignumber.equal(tokenAmount);
});
it("reject buying tokens within a few blocks", async () => {
const investor = investor3;
const investAmount = ether(10);
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.rejectedWith(EVMThrow);
});
describe(`Over max cap scenario #${ scenarioIndex }`, async () => {
const snapshot2 = new Snapshot();
before(snapshot2.captureContracts);
after(snapshot2.restoreContracts);
it("accept buying tokens over stage max cap", async () => {
const investor = investor2;
const investAmount = maxCap;
const fundedAmount = investAmount.sub(ether(110));
const rate = calcRate(baseRate, timeBonus, coeff);
const tokenAmount = fundedAmount.mul(rate);
const tokenBalance = await token.balanceOf(investor);
await advanceBlocks();
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.fulfilled;
(await token.balanceOf(investor))
.should.be.bignumber.equal(tokenBalance.add(tokenAmount));
});
it("should finalize crowdsale", async () => {
await crowdsale.finalize()
.should.be.fulfilled;
});
});
});
describe("After start time (time bonus 10%)", async () => {
const scenarioIndex = 3;
const timeBonusIndex = 2;
const timeBonus = timeBonuses[ timeBonusIndex ];
const targetTime = input.sale.rate.bonus
.time_bonuses[ timeBonusIndex - 1 ].bonus_time_stage + 1;
it(`increase time to ${ targetTime }`, async () => {
await increaseTimeTo(targetTime)
.should.be.fulfilled;
});
it("reject buying tokens for unknown account", async () => {
const investAmount = ether(10);
await sendTransaction({
from: other, to: crowdsale.address, value: investAmount, gas,
}).should.be.rejectedWith(EVMThrow);
});
it("accept buying tokens for valid account", async () => {
const investor = investor3;
const investAmount = ether(1000); // 38890 ether remains
const rate = calcRate(baseRate, timeBonus, coeff);
const tokenAmount = investAmount.mul(rate);
const tokenBalance = await token.balanceOf(investor);
await advanceBlocks();
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.fulfilled;
(await token.balanceOf(investor))
.should.be.bignumber.equal(tokenBalance.add(tokenAmount));
});
it("reject buying tokens within a few blocks", async () => {
const investor = investor3;
const investAmount = ether(1000);
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.rejectedWith(EVMThrow);
});
describe(`Over max cap scenario #${ scenarioIndex }`, async () => {
const snapshot2 = new Snapshot();
before(snapshot2.captureContracts);
after(snapshot2.restoreContracts);
it("accept buying tokens over stage max cap", async () => {
const investor = investor2;
const investAmount = maxCap;
const fundedAmount = investAmount.sub(ether(1110));
const rate = calcRate(baseRate, timeBonus, coeff);
const tokenAmount = fundedAmount.mul(rate);
const tokenBalance = await token.balanceOf(investor);
await advanceBlocks();
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.fulfilled;
(await token.balanceOf(investor))
.should.be.bignumber.equal(tokenBalance.add(tokenAmount));
});
it("should finalize crowdsale", async () => {
await crowdsale.finalize()
.should.be.fulfilled;
});
});
});
describe("After start time (time bonus 5%)", async () => {
const scenarioIndex = 4;
const timeBonusIndex = 3;
const timeBonus = timeBonuses[ timeBonusIndex ];
const targetTime = input.sale.rate.bonus
.time_bonuses[ timeBonusIndex - 1 ].bonus_time_stage + 1;
it(`increase time to ${ targetTime }`, async () => {
await increaseTimeTo(targetTime)
.should.be.fulfilled;
});
it("reject buying tokens for unknown account", async () => {
const investAmount = ether(10);
await sendTransaction({
from: other, to: crowdsale.address, value: investAmount, gas,
}).should.be.rejectedWith(EVMThrow);
});
it("accept buying tokens for valid account", async () => {
const investor = investor3;
const investAmount = ether(10000); // 28890 ether remains
const rate = calcRate(baseRate, timeBonus, coeff);
const tokenAmount = investAmount.mul(rate);
const tokenBalance = await token.balanceOf(investor);
await advanceBlocks();
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.fulfilled;
(await token.balanceOf(investor))
.should.be.bignumber.equal(tokenBalance.add(tokenAmount));
});
it("reject buying tokens within a few blocks", async () => {
const investor = investor3;
const investAmount = ether(10);
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.rejectedWith(EVMThrow);
});
describe(`Over max cap scenario #${ scenarioIndex }`, async () => {
const snapshot2 = new Snapshot();
before(snapshot2.captureContracts);
after(snapshot2.restoreContracts);
it("accept buying tokens over stage max cap", async () => {
const investor = investor2;
const investAmount = maxCap;
const fundedAmount = investAmount.sub(ether(11110));
const rate = calcRate(baseRate, timeBonus, coeff);
const tokenAmount = fundedAmount.mul(rate);
const tokenBalance = await token.balanceOf(investor);
await advanceBlocks();
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.fulfilled;
(await token.balanceOf(investor))
.should.be.bignumber.equal(tokenBalance.add(tokenAmount));
});
it("should finalize crowdsale", async () => {
await crowdsale.finalize()
.should.be.fulfilled;
});
});
});
describe("After start time (time bonus 0%)", async () => {
const scenarioIndex = 5;
const timeBonusIndex = 4;
const timeBonus = new BigNumber(0);
const targetTime = input.sale.rate.bonus
.time_bonuses[ timeBonusIndex - 1 ].bonus_time_stage + 1;
it(`increase time to ${ targetTime }`, async () => {
await increaseTimeTo(targetTime)
.should.be.fulfilled;
});
it("reject buying tokens for unknown account", async () => {
const investAmount = ether(10);
await sendTransaction({
from: other, to: crowdsale.address, value: investAmount, gas,
}).should.be.rejectedWith(EVMThrow);
});
it("accept buying tokens for valid account", async () => {
const investor = investor3;
const investAmount = ether(10000); // 18890 ether remains
const rate = calcRate(baseRate, timeBonus, coeff);
const tokenAmount = investAmount.mul(rate);
const tokenBalance = await token.balanceOf(investor);
await advanceBlocks();
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.fulfilled;
(await token.balanceOf(investor))
.should.be.bignumber.equal(tokenBalance.add(tokenAmount));
});
it("reject buying tokens within a few blocks", async () => {
const investor = investor3;
const investAmount = ether(10);
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.rejectedWith(EVMThrow);
});
describe(`Over max cap scenario #${ scenarioIndex }`, async () => {
const snapshot2 = new Snapshot();
before(snapshot2.captureContracts);
after(snapshot2.restoreContracts);
it("accept buying tokens over stage max cap", async () => {
const investor = investor2;
const investAmount = maxCap;
const fundedAmount = investAmount.sub(ether(21110));
const rate = calcRate(baseRate, timeBonus, coeff);
const tokenAmount = fundedAmount.mul(rate);
const tokenBalance = await token.balanceOf(investor);
await advanceBlocks();
await sendTransaction({
from: investor, to: crowdsale.address, value: investAmount, gas,
}).should.be.fulfilled;
(await token.balanceOf(investor))
.should.be.bignumber.equal(tokenBalance.add(tokenAmount));
});
it("should finalize crowdsale", async () => {
await crowdsale.finalize()
.should.be.fulfilled;
});
});
});
describe("After sale period ended", async () => {
const targetTime = input.sale.end_time + 1;
it(`increase time to ${ targetTime }`, async () => {
await increaseTimeTo(targetTime)
.should.be.fulfilled;
});
it("should finalize crowdsale and distribute token correctly", async () => {
const tokenDistributions = input.sale.distribution.token;
const etherDistributions = input.sale.distribution.ether;
const lockerRatio = tokenDistributions
.filter(t => t.token_holder === "locker")[ 0 ].token_ratio;
const saleRatio = tokenDistributions
.filter(t => t.token_holder === "crowdsale")[ 0 ].token_ratio;
const saleAmounts = await token.totalSupply();
const totalEtherFunded = await web3.eth.getBalance(vault.address);
await crowdsale.finalize()
.should.be.fulfilled;
const totalSupply = await token.totalSupply();
const lockerAmounts = await token.balanceOf(locker.address);
lockerAmounts.should.be.bignumber.equal(totalSupply.mul(lockerRatio).div(coeff));
saleAmounts.should.be.bignumber.equal(totalSupply.mul(saleRatio).div(coeff));
// token distribution
for (const { token_holder, token_ratio } of tokenDistributions) {
if (token_holder === "crowdsale") {
continue;
}
let addr;
if (token_holder === "locker") {
addr = locker.address;
} else {
addr = token_holder;
}
const holderBalance = await token.balanceOf(addr);
holderBalance.should.be.bignumber.equal(totalSupply.mul(token_ratio).div(coeff));
}
// ether distribution
for (let i = 0; i < 5; i++) {
const holder = (await vault.holders(i))[ 0 ];
const holderBalance = await web3.eth.getBalance(holder);
// 20% for each holder
holderBalance.should.be.bignumber.equal(totalEtherFunded.div(5));
}
});
});
});
function getInput() {
return JSON.parse('{"project_name":"RankingBall Gold","token":{"token_type":{"is_minime":true},"token_option":{"burnable":true,"pausable":true},"token_name":"RankingBall Gold","token_symbol":"RBG","decimals":18},"sale":{"max_cap":"40000000000000000000000","min_cap":"5000000000000000000000","start_time":"2018/03/12 00:00:00","end_time":"2018/04/11 00:00:00","coeff":"10000","rate":{"is_static":false,"base_rate":"50000","bonus":{"use_time_bonus":true,"use_amount_bonus":false,"time_bonuses":[{"bonus_time_stage":"2018/03/13 00:00:00","bonus_time_ratio":"2000"},{"bonus_time_stage":"2018/03/15 00:00:00","bonus_time_ratio":"1500"},{"bonus_time_stage":"2018/03/18 00:00:00","bonus_time_ratio":"1000"},{"bonus_time_stage":"2018/03/22 00:00:00","bonus_time_ratio":"500"}],"amount_bonuses":[]}},"distribution":{"token":[{"token_holder":"crowdsale","token_ratio":"2500"},{"token_holder":"0x173dF4D12d75c876656196C095f59b20aBCFa34a","_comment":"reserve1","token_ratio":"833"},{"token_holder":"0x73A5dAA05d62e6F60C5cD8d334DfDF9E59C3b14D","_comment":"reserve2","token_ratio":"833"},{"token_holder":"0xc135e061DF4AF2583B2CA2D41E37598856DB62D8","_comment":"reserve3","token_ratio":"834"},{"token_holder":"0xA3C9419b03d1Cb893eD0f2ae772d927f5Bca05A2","_comment":"martketingReserve1","token_ratio":"1000"},{"token_holder":"0x8c15c8709764b279968B647E580bbC3E370BfA87","_comment":"martketingReserve2","token_ratio":"1000"},{"token_holder":"0x2c08a528433570781A953Db7CCDed3a798C0DA9E","_comment":"martketingReserve3","token_ratio":"1000"},{"token_holder":"0x40A15bc3036293dCE89715ac7bB3090E0160e6ba","_comment":"businessPartner","token_ratio":"1000"},{"token_holder":"locker","_comment":"locker for owner and dev team","token_ratio":"1000"}],"ether":[{"ether_holder":"0x2Bc7E12243442146Bdf6dA647cA1feF3EEc04546","ether_ratio":"2000"},{"ether_holder":"0x80e21663Ada7b4929AD67C71E3afC2f77662790D","ether_ratio":"2000"},{"ether_holder":"0x2d9eD0C934B865Deba5eD192225c6F3a49d9e1E7","ether_ratio":"2000"},{"ether_holder":"multisig0","ether_ratio":"2000"},{"ether_holder":"multisig1","ether_ratio":"2000"}]},"stages":[{"start_time":"2018/03/12 00:00:00","end_time":"2018/04/11 00:00:00","cap_ratio":"100","max_purchase_limit":"0","min_purchase_limit":"0","kyc":true}],"valid_purchase":{"max_purchase_limit":"0","min_purchase_limit":"0","block_interval":5},"new_token_owner":"0x557678cf28594495ef4b08a6447726f931f8d787"},"multisig":{"use_multisig":true,"infos":[{"num_required":2,"owners":["0xbB4774A4acCe63Bbbc564dc5C77886B7257DE9b8","0x55E628D4bAb3Aa9aB593717E2a15690254Ed1B04","0x977035241e4BED626965ab21c93ef4eb342b7BDD"]},{"num_required":2,"owners":["0xE21cC11659F79c2D1d185619fD32Dc2C0120D4Ac","0x9DE692A1F1F22e4Be4A890B3adEc0D31E99B6D92","0xEb39b26B44b29A29e55dE2bAaBB5C9b2bf867e2c"]}]},"locker":{"use_locker":true,"beneficiaries":[{"address":"0xC8bF96c3E4db618BE3748d0279dbc9A07Da46E38","ratio":"5000","is_straight":true,"release":[{"release_time":"2018/04/11 00:00:00","release_ratio":"0"},{"release_time":"2020/04/11 00:00:00","release_ratio":"10000"}]},{"address":"0x4aaF7972b9277A100Cf22027261b4333eb3417Bc","ratio":"5000","is_straight":true,"release":[{"release_time":"2018/04/11 00:00:00","release_ratio":"5000"},{"release_time":"2020/04/11 00:00:00","release_ratio":"10000"}]}]}}');
}
function calcRate(baseRate, bonus, coeff) {
const r = new BigNumber(baseRate);
const b = new BigNumber(bonus);
const c = new BigNumber(coeff);
return r.mul(b.add(c)).div(c);
}