Skip to content
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

Refundable post delivery crowdsale #1543

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 7 additions & 15 deletions contracts/crowdsale/distribution/RefundableCrowdsale.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,13 @@ import "../../payment/escrow/RefundEscrow.sol";

/**
* @title RefundableCrowdsale
* @dev Extension of Crowdsale contract that adds a funding goal, and
* the possibility of users getting a refund if goal is not met.
* WARNING: note that if you allow tokens to be traded before the goal
* is met, then an attack is possible in which the attacker purchases
* tokens from the crowdsale and when they sees that the goal is
* unlikely to be met, they sell their tokens (possibly at a discount).
* The attacker will be refunded when the crowdsale is finalized, and
* the users that purchased from them will be left with worthless
* tokens. There are many possible ways to avoid this, like making the
* the crowdsale inherit from PostDeliveryCrowdsale, or imposing
* restrictions on token trading until the crowdsale is finalized.
* This is being discussed in
* https://github.com/OpenZeppelin/openzeppelin-solidity/issues/877
* This contract will be updated when we agree on a general solution
* for this problem.
* @dev Extension of Crowdsale contract that adds a funding goal, and the possibility of users getting a refund if goal
* is not met.
*
* Deprecated, use RefundablePostDeliveryCrowdsale instead. Note that if you allow tokens to be traded before the goal
* is met, then an attack is possible in which the attacker purchases tokens from the crowdsale and when they sees that
* the goal is unlikely to be met, they sell their tokens (possibly at a discount). The attacker will be refunded when
* the crowdsale is finalized, and the users that purchased from them will be left with worthless tokens.
*/
contract RefundableCrowdsale is FinalizableCrowdsale {
using SafeMath for uint256;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
pragma solidity ^0.4.24;

import "./RefundableCrowdsale.sol";
import "./PostDeliveryCrowdsale.sol";


/**
* @title RefundablePostDeliveryCrowdsale
* @dev Extension of RefundableCrowdsale contract that only delivers the tokens
* once the crowdsale has closed and the goal met, preventing refunds to be issued
* to token holders.
*/
contract RefundablePostDeliveryCrowdsale is RefundableCrowdsale, PostDeliveryCrowdsale {
function withdrawTokens(address beneficiary) public {
require(finalized());
require(goalReached());

super.withdrawTokens(beneficiary);
}
}
4 changes: 2 additions & 2 deletions contracts/mocks/RefundableCrowdsaleImpl.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pragma solidity ^0.4.24;

import "../token/ERC20/ERC20Mintable.sol";
import "../token/ERC20/IERC20.sol";
import "../crowdsale/distribution/RefundableCrowdsale.sol";

contract RefundableCrowdsaleImpl is RefundableCrowdsale {
Expand All @@ -9,7 +9,7 @@ contract RefundableCrowdsaleImpl is RefundableCrowdsale {
uint256 closingTime,
uint256 rate,
address wallet,
ERC20Mintable token,
IERC20 token,
uint256 goal
)
public
Expand Down
20 changes: 20 additions & 0 deletions contracts/mocks/RefundablePostDeliveryCrowdsaleImpl.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
pragma solidity ^0.4.24;

import "../token/ERC20/IERC20.sol";
import "../crowdsale/distribution/RefundablePostDeliveryCrowdsale.sol";

contract RefundablePostDeliveryCrowdsaleImpl is RefundablePostDeliveryCrowdsale {
constructor (
uint256 openingTime,
uint256 closingTime,
uint256 rate,
address wallet,
IERC20 token,
uint256 goal
)
public
Crowdsale(rate, wallet, token)
TimedCrowdsale(openingTime, closingTime)
RefundableCrowdsale(goal)
{}
}
103 changes: 103 additions & 0 deletions test/crowdsale/RefundablePostDeliveryCrowdsale.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
const time = require('../helpers/time');
const shouldFail = require('../helpers/shouldFail');
const { ether } = require('../helpers/ether');

const BigNumber = web3.BigNumber;

require('chai')
.use(require('chai-bignumber')(BigNumber))
.should();

const RefundablePostDeliveryCrowdsaleImpl = artifacts.require('RefundablePostDeliveryCrowdsaleImpl');
const SimpleToken = artifacts.require('SimpleToken');

contract('RefundablePostDeliveryCrowdsale', function ([_, investor, wallet, purchaser]) {
const rate = new BigNumber(1);
const tokenSupply = new BigNumber('1e22');
const goal = ether(100);

before(async function () {
// Advance to the next block to correctly read time in the solidity "now" function interpreted by ganache
await time.advanceBlock();
});

beforeEach(async function () {
this.openingTime = (await time.latest()) + time.duration.weeks(1);
this.closingTime = this.openingTime + time.duration.weeks(1);
this.afterClosingTime = this.closingTime + time.duration.seconds(1);
this.token = await SimpleToken.new();
this.crowdsale = await RefundablePostDeliveryCrowdsaleImpl.new(
this.openingTime, this.closingTime, rate, wallet, this.token.address, goal
);
await this.token.transfer(this.crowdsale.address, tokenSupply);
});

context('after opening time', function () {
beforeEach(async function () {
await time.increaseTo(this.openingTime);
});

context('with bought tokens below the goal', function () {
const value = goal.sub(1);

beforeEach(async function () {
await this.crowdsale.buyTokens(investor, { value: value, from: purchaser });
});

it('does not immediately deliver tokens to beneficiaries', async function () {
(await this.crowdsale.balanceOf(investor)).should.be.bignumber.equal(value);
(await this.token.balanceOf(investor)).should.be.bignumber.equal(0);
});

it('does not allow beneficiaries to withdraw tokens before crowdsale ends', async function () {
await shouldFail.reverting(this.crowdsale.withdrawTokens(investor));
});

context('after closing time and finalization', function () {
beforeEach(async function () {
await time.increaseTo(this.afterClosingTime);
await this.crowdsale.finalize();
});

it('rejects token withdrawals', async function () {
await shouldFail.reverting(this.crowdsale.withdrawTokens(investor));
});
});
});

context('with bought tokens matching the goal', function () {
const value = goal;

beforeEach(async function () {
await this.crowdsale.buyTokens(investor, { value: value, from: purchaser });
});

it('does not immediately deliver tokens to beneficiaries', async function () {
(await this.crowdsale.balanceOf(investor)).should.be.bignumber.equal(value);
(await this.token.balanceOf(investor)).should.be.bignumber.equal(0);
});

it('does not allow beneficiaries to withdraw tokens before crowdsale ends', async function () {
await shouldFail.reverting(this.crowdsale.withdrawTokens(investor));
});

context('after closing time and finalization', function () {
beforeEach(async function () {
await time.increaseTo(this.afterClosingTime);
await this.crowdsale.finalize();
});

it('allows beneficiaries to withdraw tokens', async function () {
await this.crowdsale.withdrawTokens(investor);
(await this.crowdsale.balanceOf(investor)).should.be.bignumber.equal(0);
(await this.token.balanceOf(investor)).should.be.bignumber.equal(value);
});

it('rejects multiple withdrawals', async function () {
await this.crowdsale.withdrawTokens(investor);
await shouldFail.reverting(this.crowdsale.withdrawTokens(investor));
});
});
});
});
});