-
Notifications
You must be signed in to change notification settings - Fork 12.4k
Composite crowdsale (WIP) #426
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
Closed
jakub-wojciechowski
wants to merge
1
commit into
OpenZeppelin:master
from
jakub-wojciechowski:composite-crowdsale
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| pragma solidity ^0.4.11; | ||
|
|
||
| import '../token/MintableToken.sol'; | ||
| import '../math/SafeMath.sol'; | ||
| import './TokenDistributionStrategy.sol'; | ||
|
|
||
| /** | ||
| * @title CompositeCrowdsale | ||
| * @dev CompositeCrowdsale is a base contract for managing a token crowdsale. | ||
| * Contrary to a classic crowdsale, it favours composition over inheritance. | ||
| * | ||
| * Crowdsale behaviour can be modified by specifying TokenDistributionStrategy | ||
| * which is a dedicated smart contract that delegates all of the logic managing | ||
| * token distribution. | ||
| * | ||
| * CompositeCrowdsale is at the WIP stage and is meant to illustrate composition | ||
| * approach for managing crowdsale logic. It shouldn't be used in production code | ||
| * before necessary upgrades and testing. | ||
| */ | ||
| contract CompositeCrowdsale { | ||
| using SafeMath for uint256; | ||
|
|
||
| // The token being sold | ||
| TokenDistributionStrategy public tokenDistribution; | ||
|
|
||
| // start and end timestamps where investments are allowed (both inclusive) | ||
| uint256 public startTime; | ||
| uint256 public endTime; | ||
|
|
||
| // address where funds are collected | ||
| address public wallet; | ||
|
|
||
| // how many token units a buyer gets per wei | ||
| uint256 public rate; | ||
|
|
||
| // amount of raised money in wei | ||
| uint256 public weiRaised; | ||
|
|
||
| /** | ||
| * event for token purchase logging | ||
| * @param purchaser who paid for the tokens | ||
| * @param beneficiary who got the tokens | ||
| * @param value weis paid for purchase | ||
| * @param amount amount of tokens purchased | ||
| */ | ||
| event TokenPurchase(address indexed purchaser, address indexed beneficiary, uint256 value, uint256 amount); | ||
|
|
||
|
|
||
| function CompositeCrowdsale(uint256 _startTime, uint256 _endTime, uint256 _rate, address _wallet, TokenDistributionStrategy _tokenDistribution) { | ||
| require(_startTime >= now); | ||
| require(_endTime >= _startTime); | ||
| require(_rate > 0); | ||
| require(_wallet != 0x0); | ||
|
|
||
| tokenDistribution = _tokenDistribution; | ||
| tokenDistribution.initializeDistribution(this); | ||
|
|
||
| startTime = _startTime; | ||
| endTime = _endTime; | ||
| rate = _rate; | ||
| wallet = _wallet; | ||
| } | ||
|
|
||
|
|
||
| // fallback function can be used to buy tokens | ||
| function () payable { | ||
| buyTokens(msg.sender); | ||
| } | ||
|
|
||
| // low level token purchase function | ||
| function buyTokens(address beneficiary) payable { | ||
| require(beneficiary != 0x0); | ||
| require(validPurchase()); | ||
|
|
||
| uint256 weiAmount = msg.value; | ||
|
|
||
| // calculate token amount to be created | ||
| uint256 tokens = weiAmount.mul(rate); | ||
|
|
||
| // update state | ||
| weiRaised = weiRaised.add(weiAmount); | ||
|
|
||
| tokenDistribution.distributeTokens(beneficiary, tokens); | ||
| TokenPurchase(msg.sender, beneficiary, weiAmount, tokens); | ||
|
|
||
| forwardFunds(); | ||
| } | ||
|
|
||
| // send ether to the fund collection wallet | ||
| // override to create custom fund forwarding mechanisms | ||
| function forwardFunds() internal { | ||
| wallet.transfer(msg.value); | ||
| } | ||
|
|
||
| // @return true if the transaction can buy tokens | ||
| function validPurchase() internal constant returns (bool) { | ||
| bool withinPeriod = now >= startTime && now <= endTime; | ||
| bool nonZeroPurchase = msg.value != 0; | ||
| return withinPeriod && nonZeroPurchase; | ||
| } | ||
|
|
||
| // @return true if crowdsale event has ended | ||
| function hasEnded() public constant returns (bool) { | ||
| return now > endTime; | ||
| } | ||
|
|
||
|
|
||
| } |
42 changes: 42 additions & 0 deletions
42
contracts/crowdsale/FixedPoolTokenDistributionStrategy.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| pragma solidity ^0.4.11; | ||
|
|
||
| import '../examples/SimpleToken.sol'; | ||
| import './TokenDistributionStrategy.sol'; | ||
| import '../token/ERC20.sol'; | ||
| import '../math/SafeMath.sol'; | ||
|
|
||
| /** | ||
| * @title FixedRateTokenDistributionStrategy | ||
| * @dev Strategy that distributes a fixed number of tokens among the contributors. | ||
| * It's done in two steps. First, it registers all of the contributions while the sale is active. | ||
| * After the crowdsale has ended the contract compensate buyers proportionally to their contributions. | ||
| */ | ||
| contract FixedPoolTokenDistributionStrategy is TokenDistributionStrategy { | ||
| using SafeMath for uint256; | ||
|
|
||
| // The token being sold | ||
| ERC20 token; | ||
| mapping(address => uint256) contributions; | ||
| uint256 totalContributed; | ||
|
|
||
| function FixedPoolTokenDistributionStrategy(ERC20 _token) { | ||
| token = _token; | ||
| } | ||
|
|
||
| function distributeTokens(address _beneficiary, uint256 _amount) onlyCrowdsale { | ||
| contributions[_beneficiary] = contributions[_beneficiary].add(_amount); | ||
| totalContributed = totalContributed.add(_amount); | ||
| } | ||
|
|
||
| function compensate(address _beneficiary) { | ||
| require(crowdsale.hasEnded()); | ||
| uint256 amount = contributions[_beneficiary].mul(token.totalSupply()).div(totalContributed); | ||
| if (token.transfer(_beneficiary, amount)) { | ||
| contributions[_beneficiary] = 0; | ||
| } | ||
| } | ||
|
|
||
| function getToken() constant returns(ERC20) { | ||
| return token; | ||
| } | ||
| } | ||
30 changes: 30 additions & 0 deletions
30
contracts/crowdsale/FixedRateTokenDistributionStrategy.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| pragma solidity ^0.4.11; | ||
|
|
||
| import '../token/MintableToken.sol'; | ||
| import './TokenDistributionStrategy.sol'; | ||
| import '../token/ERC20.sol'; | ||
|
|
||
| /** | ||
| * @title FixedRateTokenDistributionStrategy | ||
| * @dev Strategy that grants a fixed number of tokens per donation value | ||
| * Final number of tokens is not defined as it depends on the total amount | ||
| * of contributions that are collected during the crowdsale. | ||
| */ | ||
| contract FixedRateTokenDistributionStrategy is TokenDistributionStrategy { | ||
|
|
||
| // The token being sold | ||
| MintableToken token; | ||
|
|
||
| function initializeDistribution(CompositeCrowdsale _crowdsale) { | ||
| super.initializeDistribution(_crowdsale); | ||
| token = new MintableToken(); | ||
| } | ||
|
|
||
| function distributeTokens(address beneficiary, uint amount) onlyCrowdsale { | ||
| token.mint(beneficiary, amount); | ||
| } | ||
|
|
||
| function getToken() constant returns(ERC20) { | ||
| return token; | ||
| } | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably will need to dissociate ownership from minting right as we talked about in slack. |
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| pragma solidity ^0.4.11; | ||
|
|
||
| import '../token/ERC20.sol'; | ||
| import './CompositeCrowdsale.sol'; | ||
|
|
||
|
|
||
| /** | ||
| * @title TokenDistributionStrategy | ||
| * @dev Base abstract contract defining methods that control token distribution | ||
| */ | ||
| contract TokenDistributionStrategy { | ||
|
|
||
| CompositeCrowdsale crowdsale; | ||
|
|
||
| modifier onlyCrowdsale() { | ||
| require(msg.sender == address(crowdsale)); | ||
| _; | ||
| } | ||
|
|
||
| function initializeDistribution(CompositeCrowdsale _crowdsale) { | ||
| require(crowdsale == address(0)); | ||
| require(_crowdsale != address(0)); | ||
| crowdsale = _crowdsale; | ||
| } | ||
|
|
||
| function distributeTokens(address beneficiary, uint amount) onlyCrowdsale {} | ||
|
|
||
| function getToken() constant returns(ERC20); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import ether from './helpers/ether' | ||
| import {advanceBlock} from './helpers/advanceToBlock' | ||
| import {increaseTimeTo, duration} from './helpers/increaseTime' | ||
| import latestTime from './helpers/latestTime' | ||
|
|
||
| const BigNumber = web3.BigNumber | ||
|
|
||
| const should = require('chai') | ||
| .use(require('chai-as-promised')) | ||
| .use(require('chai-bignumber')(BigNumber)) | ||
| .should() | ||
|
|
||
| const CompositeCrowdsale = artifacts.require('CompositeCrowdsale') | ||
| const FixedRateTokenDistribution = artifacts.require('FixedRateTokenDistributionStrategy') | ||
| const Token = artifacts.require('ERC20') | ||
|
|
||
| const FixedPoolTokenDistribution = artifacts.require('FixedPoolTokenDistributionStrategy') | ||
| const SimpleToken = artifacts.require('SimpleToken') | ||
|
|
||
| contract('CompositeCrowdsale', function ([_, investor, wallet]) { | ||
|
|
||
| const RATE = new BigNumber(1000); | ||
|
|
||
| before(async function() { | ||
| //Advance to the next block to correctly read time in the solidity "now" function interpreted by testrpc | ||
| await advanceBlock(); | ||
| }) | ||
|
|
||
| describe('Fixed Rate Distribution', function () { | ||
|
|
||
| beforeEach(async function () { | ||
| this.startTime = latestTime() + duration.weeks(1); | ||
| this.endTime = this.startTime + duration.weeks(1); | ||
| this.afterEndTime = this.endTime + duration.seconds(1); | ||
|
|
||
| this.tokenDistribution = await FixedRateTokenDistribution.new(); | ||
| this.crowdsale = await CompositeCrowdsale.new(this.startTime, this.endTime, RATE, wallet, this.tokenDistribution.address) | ||
| this.token = Token.at(await this.tokenDistribution.getToken.call()); | ||
| }) | ||
|
|
||
| beforeEach(async function () { | ||
| await increaseTimeTo(this.startTime) | ||
| }) | ||
|
|
||
| it('should accept payments and mint tokens during the sale', async function () { | ||
| const investmentAmount = ether(1); | ||
| const expectedTokenAmount = RATE.mul(investmentAmount); | ||
|
|
||
| let tx = await this.crowdsale.buyTokens(investor, {value: investmentAmount, from: investor}).should.be.fulfilled; | ||
| console.log("*** COMPOSITION FIXED RATE: " + tx.receipt.gasUsed + " gas used."); | ||
|
|
||
| (await this.token.balanceOf(investor)).should.be.bignumber.equal(expectedTokenAmount); | ||
| (await this.token.totalSupply()).should.be.bignumber.equal(expectedTokenAmount); | ||
| }); | ||
|
|
||
| }); | ||
|
|
||
| describe('Fixed Pool Distribution', function () { | ||
|
|
||
| beforeEach(async function () { | ||
| this.startTime = latestTime() + duration.weeks(1); | ||
| this.endTime = this.startTime + duration.weeks(1); | ||
| this.afterEndTime = this.endTime + duration.seconds(1); | ||
|
|
||
| const fixedPoolToken = await SimpleToken.new(); | ||
| const totalSupply = await fixedPoolToken.totalSupply(); | ||
| this.tokenDistribution = await FixedPoolTokenDistribution.new(fixedPoolToken.address); | ||
| await fixedPoolToken.transfer(this.tokenDistribution.address, totalSupply); | ||
| this.crowdsale = await CompositeCrowdsale.new(this.startTime, this.endTime, RATE, wallet, this.tokenDistribution.address) | ||
| this.token = Token.at(await this.tokenDistribution.getToken.call()); | ||
| }) | ||
|
|
||
| beforeEach(async function () { | ||
| await increaseTimeTo(this.startTime) | ||
| }) | ||
|
|
||
| it('should buy tokens and compensate contributors after the sale', async function () { | ||
| const investmentAmount = ether(1); | ||
| let tx = await this.crowdsale.buyTokens(investor, {value: investmentAmount, from: investor}).should.be.fulfilled; | ||
| console.log("*** COMPOSITION FIXED POOL: " + tx.receipt.gasUsed + " gas used."); | ||
|
|
||
| await increaseTimeTo(this.afterEndTime); | ||
| await this.tokenDistribution.compensate(investor).should.be.fulfilled; | ||
| const totalSupply = await this.token.totalSupply(); | ||
| (await this.token.balanceOf(investor)).should.be.bignumber.equal(totalSupply); | ||
| }) | ||
|
|
||
|
|
||
| }); | ||
|
|
||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not the good title