diff --git a/contracts/Hatch.sol b/contracts/Hatch.sol index c2aee0f..1a31969 100644 --- a/contracts/Hatch.sol +++ b/contracts/Hatch.sol @@ -38,8 +38,6 @@ contract Hatch is EtherTokenConstant, IsContract, AragonApp, IACLOracle { string private constant ERROR_INVALID_PCT = "HATCH_INVALID_PCT"; string private constant ERROR_INVALID_STATE = "HATCH_INVALID_STATE"; string private constant ERROR_INVALID_CONTRIBUTE_VALUE = "HATCH_INVALID_CONTRIBUTE_VALUE"; - string private constant ERROR_INSUFFICIENT_BALANCE = "HATCH_INSUFFICIENT_BALANCE"; - string private constant ERROR_INSUFFICIENT_ALLOWANCE = "HATCH_INSUFFICIENT_ALLOWANCE"; string private constant ERROR_NOTHING_TO_REFUND = "HATCH_NOTHING_TO_REFUND"; string private constant ERROR_TOKEN_TRANSFER_REVERTED = "HATCH_TOKEN_TRANSFER_REVERTED"; @@ -59,13 +57,13 @@ contract Hatch is EtherTokenConstant, IsContract, AragonApp, IACLOracle { uint256 public minGoal; uint256 public maxGoal; - uint64 public period; uint256 public exchangeRate; + uint64 public period; uint64 public vestingCliffPeriod; uint64 public vestingCompletePeriod; + uint64 public openDate; uint256 public supplyOfferedPct; uint256 public fundingForBeneficiaryPct; - uint64 public openDate; bool public isClosed; uint64 public vestingCliffDate; @@ -123,8 +121,8 @@ contract Hatch is EtherTokenConstant, IsContract, AragonApp, IACLOracle { require(_maxGoal >= _minGoal, ERROR_INVALID_MAX_GOAL); require(_period > 0, ERROR_INVALID_TIME_PERIOD); require(_exchangeRate > 0, ERROR_INVALID_EXCHANGE_RATE); - require(_vestingCliffPeriod > _period, ERROR_INVALID_TIME_PERIOD); - require(_vestingCompletePeriod > _vestingCliffPeriod, ERROR_INVALID_TIME_PERIOD); + require(_vestingCliffPeriod >= _period, ERROR_INVALID_TIME_PERIOD); + require(_vestingCompletePeriod >= _vestingCliffPeriod, ERROR_INVALID_TIME_PERIOD); require(_supplyOfferedPct > 0 && _supplyOfferedPct <= PPM, ERROR_INVALID_PCT); require(_fundingForBeneficiaryPct >= 0 && _fundingForBeneficiaryPct <= PPM, ERROR_INVALID_PCT); @@ -250,6 +248,13 @@ contract Hatch is EtherTokenConstant, IsContract, AragonApp, IACLOracle { return vestingCompleteDate != 0 && getTimestamp64() >= vestingCompleteDate; } + /** + * @dev Turns off fund recovery for contribution token when the hatch is ongoing + */ + function allowRecoverability(address _token) public view isInitialized returns (bool) { + return _token != contributionToken || state() == State.Pending || state() == State.Closed; + } + /***** internal functions *****/ function _timeSinceOpen() internal view returns (uint64) { @@ -281,13 +286,11 @@ contract Hatch is EtherTokenConstant, IsContract, AragonApp, IACLOracle { function _contribute(address _contributor, uint256 _value) internal { uint256 value = totalRaised.add(_value) > maxGoal ? maxGoal.sub(totalRaised) : _value; if (contributionToken == ETH && _value > value) { - msg.sender.transfer(_value.sub(value)); + msg.sender.call.value(_value.sub(value)); } // (contributor) ~~~> contribution tokens ~~~> (hatch) if (contributionToken != ETH) { - require(ERC20(contributionToken).balanceOf(_contributor) >= value, ERROR_INSUFFICIENT_BALANCE); - require(ERC20(contributionToken).allowance(_contributor, address(this)) >= value, ERROR_INSUFFICIENT_ALLOWANCE); _transfer(contributionToken, _contributor, address(this), value); } // (mint ✨) ~~~> project tokens ~~~> (contributor) @@ -362,7 +365,7 @@ contract Hatch is EtherTokenConstant, IsContract, AragonApp, IACLOracle { if (_token == ETH) { require(_from == address(this), ERROR_TOKEN_TRANSFER_REVERTED); require(_to != address(this), ERROR_TOKEN_TRANSFER_REVERTED); - _to.transfer(_amount); + _to.call.value(_amount); } else { if (_from == address(this)) { require(ERC20(_token).safeTransfer(_to, _amount), ERROR_TOKEN_TRANSFER_REVERTED); diff --git a/package.json b/package.json index a9d87ce..71b3b0e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ ], "dependencies": { "@aragon/apps-token-manager": "^2.1.0", - "@aragon/os": "4.2.1" + "@aragon/os": "^4.4.0" }, "devDependencies": { "@aragon/minime": "^1.0.0", diff --git a/test/States.test.js b/test/States.test.js index 0537823..03d1cac 100644 --- a/test/States.test.js +++ b/test/States.test.js @@ -1,6 +1,6 @@ const { HATCH_PERIOD, HATCH_MAX_GOAL, HATCH_STATE, HATCH_MIN_GOAL } = require('./helpers/constants') const { prepareDefaultSetup, defaultDeployParams, initializeHatch } = require('./common/deploy') -const { now } = require('./common/utils') +const { now, tokenExchangeRate } = require('./common/utils') const getState = async test => { return (await test.hatch.state()).toNumber() @@ -21,6 +21,10 @@ contract('Hatch, states validation', ([anyone, appManager, buyer]) => { assert.equal(await getState(this), HATCH_STATE.PENDING) }) + it('It can escape hatch', async() => { + assert.isTrue(await this.hatch.allowRecoverability(this.contributionToken.address)); + }) + describe('When the sale is started', () => { before(async () => { if (startDate == 0) { @@ -34,6 +38,10 @@ contract('Hatch, states validation', ([anyone, appManager, buyer]) => { assert.equal(await getState(this), HATCH_STATE.FUNDING) }) + it('It cannot escape hatch', async() => { + assert.isFalse(await this.hatch.allowRecoverability(this.contributionToken.address)); + }) + describe('When the funding period is still running', () => { before(async () => { this.hatch.mockSetTimestamp(startDate + HATCH_PERIOD / 2) @@ -60,6 +68,10 @@ contract('Hatch, states validation', ([anyone, appManager, buyer]) => { it('The state is Refunding', async () => { assert.equal(await getState(this), HATCH_STATE.REFUNDING) }) + + it('It cannot escape hatch', async() => { + assert.isFalse(await this.hatch.allowRecoverability(this.contributionToken.address)); + }) }) }) @@ -112,6 +124,10 @@ contract('Hatch, states validation', ([anyone, appManager, buyer]) => { it('The state is Closed', async () => { assert.equal(await getState(this), HATCH_STATE.CLOSED) }) + + it('It can escape hatch', async() => { + assert.isTrue(await this.hatch.allowRecoverability(this.contributionToken.address)); + }) }) }) })