From 29d1b09bde313257849a1a75867d9702f01a20c8 Mon Sep 17 00:00:00 2001 From: Anthony Akentiev Date: Sat, 21 Jul 2018 14:13:08 +0300 Subject: [PATCH 01/10] Adding PreserveBalancesOnTransferToken --- .../ERC20/PreserveBalancesOnTransferToken.sol | 162 +++++++ .../PreserveBalancesOnTransferToken.test.js | 400 ++++++++++++++++++ 2 files changed, 562 insertions(+) create mode 100644 contracts/token/ERC20/PreserveBalancesOnTransferToken.sol create mode 100644 test/token/ERC20/PreserveBalancesOnTransferToken.test.js diff --git a/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol b/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol new file mode 100644 index 00000000000..7a7f19a9ff9 --- /dev/null +++ b/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol @@ -0,0 +1,162 @@ +pragma solidity ^0.4.24; + +import "./MintableToken.sol"; +import "./BurnableToken.sol"; + +/** + * @title PreserveBalancesOnTransferToken (Copy-on-Write) token + * @author Based on code by Thetta DAO Framework: https://github.com/Thetta/Thetta-DAO-Framework/ + * @dev Token that can preserve the balances after some EVENT happens (voting is started, didivends are calculated, etc) + * without blocking the transfers! Please notice that EVENT in this case has nothing to do with Ethereum events. + * + * Example of usage (pseudocode): + * token.mint(ADDRESS_A, 100); + * assert.equal(token.balanceOf(ADDRESS_A), 100); + * assert.equal(token.balanceOf(ADDRESS_B), 0); + * + * uint someEventID_1 = token.startNewEvent(); + * token.transfer(ADDRESS_A, ADDRESS_B, 30); + * + * assert.equal(token.balanceOf(ADDRESS_A), 70); + * assert.equal(token.balanceOf(ADDRESS_B), 30); + * + * assert.equal(token.getBalanceAtEvent(someEventID_1, ADDRESS_A), 100); + * assert.equal(token.getBalanceAtEvent(someEventID_1, ADDRESS_B), 0); + * + * token.finishEvent(someEventID_1); + */ +contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { + struct Holder { + uint256 balance; + uint lastUpdateTime; + } + + struct Event { + mapping (address => Holder) holders; + + bool isEventInProgress; + uint eventStartTime; + } + mapping (uint => Event) events; + + event EventStarted(address indexed _address, uint _eventID); + event EventFinished(address indexed _address, uint _eventID); + +// BasicToken overrides: + /** + * @dev Transfer token for a specified address + * @param _to The address to transfer to. + * @param _value The amount to be transferred. + */ + function transfer(address _to, uint256 _value) public returns (bool) { + updateCopyOnWriteMaps(msg.sender, _to); + return super.transfer(_to, _value); + } + +// StandardToken overrides: + /** + * @dev Transfer tokens from one address to another + * @param _from address The address which you want to send tokens from + * @param _to address The address which you want to transfer to + * @param _value uint256 the amount of tokens to be transferred + */ + function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { + updateCopyOnWriteMaps(_from, _to); + return super.transferFrom(_from, _to, _value); + } + +// MintableToken overrides: + /** + * @dev Function to mint tokens + * @param _to The address that will receive the minted tokens. + * @param _amount The amount of tokens to mint. + * @return A boolean that indicates if the operation was successful. + */ + function mint(address _to, uint256 _amount) canMint onlyOwner public returns(bool){ + updateCopyOnWriteMap(_to); + return super.mint(_to, _amount); + } + +// BurnableToken overrides: + /** @dev This is an override of internal method! Public method burn() calls _burn() automatically + * (see BurnableToken implementation) + */ + function _burn(address _who, uint256 _value) internal { + updateCopyOnWriteMap(_who); + super._burn(_who, _value); + } + +// PreserveBalancesOnTransferToken - new methods: + /** + * @dev Function to signal that some event happens (dividends are calculated, voting, etc) + * so we need to start preserving balances AT THE time this event happened. + * @return An index of the event started. + */ + function startNewEvent() public onlyOwner returns(uint){ + for(uint i = 0; i < 20; i++){ + if(!events[i].isEventInProgress){ + events[i].isEventInProgress = true; + events[i].eventStartTime = now; + + emit EventStarted(msg.sender, i); + return i; + } + } + revert(); //all slots busy at the moment + } + + /** + * @dev Function to signal that some event is finished + * @param _eventID An index of the event that was previously returned by startNewEvent(). + */ + function finishEvent(uint _eventID) public onlyOwner { + require(events[_eventID].isEventInProgress); + events[_eventID].isEventInProgress = false; + + emit EventFinished(msg.sender, _eventID); + } + + /** + * @dev Returns the balance of the address _for at the time event _eventID happened + * !!! WARNING !!! + * We do not give STRONG guarantees. The return value is time-dependent: + * If startNewEvent() is called and then immediately getBalanceAtEventStart() -> it CAN return wrong data + * In case time between these calls has passed -> the return value is ALWAYS correct. + * + * Please see tests. + * return Token balance (when the event started, but not a CURRENT balanceOf()!) + */ + function getBalanceAtEventStart(uint _eventID, address _for) public view returns(uint256) { + require(events[_eventID].isEventInProgress); + + if(!isBalanceWasChangedAfterEventStarted(_eventID, _for)){ + return balances[_for]; + } + + return events[_eventID].holders[_for].balance; + } + +// Internal methods: + function updateCopyOnWriteMaps(address _from, address _to) internal { + updateCopyOnWriteMap(_to); + updateCopyOnWriteMap(_from); + } + + function updateCopyOnWriteMap(address _for) internal { + for(uint i = 0; i < 20; i++){ + bool res = isNeedToUpdateBalancesMap(i, _for); + if(res){ + events[i].holders[_for].balance = balances[_for]; + events[i].holders[_for].lastUpdateTime = now; + } + } + } + + function isNeedToUpdateBalancesMap(uint _eventID, address _for) internal view returns(bool) { + return events[_eventID].isEventInProgress && !isBalanceWasChangedAfterEventStarted(_eventID, _for); + } + + function isBalanceWasChangedAfterEventStarted(uint _eventID, address _for) internal view returns(bool){ + return (events[_eventID].holders[_for].lastUpdateTime >= events[_eventID].eventStartTime); + } +} diff --git a/test/token/ERC20/PreserveBalancesOnTransferToken.test.js b/test/token/ERC20/PreserveBalancesOnTransferToken.test.js new file mode 100644 index 00000000000..e6a0399e72f --- /dev/null +++ b/test/token/ERC20/PreserveBalancesOnTransferToken.test.js @@ -0,0 +1,400 @@ +const BigNumber = web3.BigNumber; +const PreserveBalancesOnTransferToken = artifacts.require('PreserveBalancesOnTransferToken'); + +// Increases ganache time by the passed duration in seconds +function increaseTime (duration) { + const id = Date.now(); + + return new Promise((resolve, reject) => { + web3.currentProvider.sendAsync({ + jsonrpc: '2.0', + method: 'evm_increaseTime', + params: [duration], + id: id, + }, err1 => { + if (err1) return reject(err1); + + web3.currentProvider.sendAsync({ + jsonrpc: '2.0', + method: 'evm_mine', + id: id + 1, + }, (err2, res) => { + return err2 ? reject(err2) : resolve(res); + }); + }); + }); +} + + +function increaseTimeTo (target) { + let now = web3.eth.getBlock('latest').timestamp; + if (target < now) throw Error(`Cannot increase current time(${now}) to a moment in the past(${target})`); + let diff = target - now; + return increaseTime(diff); +} + +const duration = { + seconds: function (val) { return val; }, + minutes: function (val) { return val * this.seconds(60); }, + hours: function (val) { return val * this.minutes(60); }, + days: function (val) { return val * this.hours(24); }, + weeks: function (val) { return val * this.days(7); }, + years: function (val) { return val * this.days(365); }, +}; + + +require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should(); + + contract('PreserveBalancesOnTransferToken', (accounts) => { + const creator = accounts[0]; + const account3 = accounts[3]; + const account4 = accounts[4]; + const account5 = accounts[5]; + + beforeEach(async function () { + + }); + + describe('mint', function () { + it('should fail due to not owner call', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(web3.eth.accounts[1], 1000, {from: web3.eth.accounts[1]}).should.be.rejectedWith('revert'); + }); + + it('should fail with isMintable = false', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(web3.eth.accounts[1], 1000); + }); + + it('should fail due to finishMinting() call', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.finishMinting(); + await this.token.mint(web3.eth.accounts[1], 1000).should.be.rejectedWith('revert'); + }); + + it('should pass', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(web3.eth.accounts[0], 1000); + let balance = await this.token.balanceOf(web3.eth.accounts[0]); + assert.equal(balance.toNumber(), 1000); + }); + }); + + describe('burn', function () { + it('should fail due to not owner call', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(web3.eth.accounts[1], 1000); + await this.token.burn(1000, {from: web3.eth.accounts[0]}).should.be.rejectedWith('revert'); + }); + + it('should fail due to not enough tokens in the address provided', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.burn(1000).should.be.rejectedWith('revert'); + }); + + it('should pass', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(web3.eth.accounts[0], 1000); + await this.token.burn(1000); + let balance = await this.token.balanceOf(web3.eth.accounts[0]); + assert.equal(balance.toNumber(), 0); + }); + }); + + describe('startNewEvent', function() { + it('should not allow to create > 20 separate events',async() => { + this.token = await PreserveBalancesOnTransferToken.new(); + + await this.token.startNewEvent();//1 + await this.token.startNewEvent();//2 + await this.token.startNewEvent();//3 + await this.token.startNewEvent();//4 + await this.token.startNewEvent();//5 + await this.token.startNewEvent();//6 + await this.token.startNewEvent();//7 + await this.token.startNewEvent();//8 + await this.token.startNewEvent();//9 + await this.token.startNewEvent();//10 + await this.token.startNewEvent();//11 + await this.token.startNewEvent();//12 + await this.token.startNewEvent();//13 + await this.token.startNewEvent();//14 + await this.token.startNewEvent();//15 + await this.token.startNewEvent();//16 + await this.token.startNewEvent();//17 + await this.token.startNewEvent();//18 + await this.token.startNewEvent();//19 + await this.token.startNewEvent();//20 + await this.token.startNewEvent().should.be.rejectedWith('revert'); + }); + + it('should not be possible to call by non-owner',async() => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.startNewEvent({from: account3}).should.be.rejectedWith('revert'); + }); + }); + + describe('getBalanceAtEventStart', function () { + it('should preserve balances if no transfers happened after event is started',async() => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account4, 1); + + let account4Balance = await this.token.balanceOf(account4); + let account5Balance = await this.token.balanceOf(account5); + + assert.equal(account4Balance.toNumber(), 1); + assert.equal(account5Balance.toNumber(), 0); + + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event == 'EventStarted'); + const eventID = events.filter(e => e.args._address == creator)[0].args._eventID; + + let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); + + assert.equal(account4EventBalance.toNumber(), 1); + assert.equal(account5EventBalance.toNumber(), 0); + }); + + it('should preserve balances after event is started',async() => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account4, 1); + + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event == 'EventStarted'); + const eventID = events.filter(e => e.args._address == creator)[0].args._eventID; + + await this.token.transfer(account5, 1, {from: account4}); + + let account4Balance = await this.token.balanceOf(account4); + let account5Balance = await this.token.balanceOf(account5); + + let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); + + assert.equal(account4Balance.toNumber(), 0); + assert.equal(account5Balance.toNumber(), 1); + + assert.equal(account4EventBalance.toNumber(), 1); + assert.equal(account5EventBalance.toNumber(), 0); + }); + + it('should preserve balances after event is started and mint called',async() => { + this.token = await PreserveBalancesOnTransferToken.new(); + + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event == 'EventStarted'); + const eventID = events.filter(e => e.args._address == creator)[0].args._eventID; + + await this.token.mint(account4, 1); + + let account4Balance = await this.token.balanceOf(account4); + let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + + assert.equal(account4Balance.toNumber(), 1); + assert.equal(account4EventBalance.toNumber(), 0); + }); + + it('should throw exception when trying to check balancesAtVoting after event is ended',async() => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account4, 1); + + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event == 'EventStarted'); + const eventID = events.filter(e => e.args._address == creator)[0].args._eventID; + + await this.token.transfer(account5, 1, {from: account4}); + + let account4Balance = await this.token.balanceOf(account4); + let account5Balance = await this.token.balanceOf(account5); + + assert.equal(account4Balance.toNumber(), 0); + assert.equal(account5Balance.toNumber(), 1); + + await this.token.finishEvent(eventID); + + account4Balance = await this.token.balanceOf(account4); + account5Balance = await this.token.balanceOf(account5); + + assert.equal(account4Balance.toNumber(), 0); + assert.equal(account5Balance.toNumber(), 1); + + let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4).should.be.rejectedWith('revert'); + }); + + it('should preserve balances after event is started and transferFrom is called',async() => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account4, 1); + + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event == 'EventStarted'); + const eventID = events.filter(e => e.args._address == creator)[0].args._eventID; + + await this.token.approve(account3, 1, {from: account4}); + await this.token.transferFrom(account4, account5, 1, {from: account3}); + + let account4Balance = await this.token.balanceOf(account4); + let account5Balance = await this.token.balanceOf(account5); + + let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); + + assert.equal(account4Balance.toNumber(), 0); + assert.equal(account5Balance.toNumber(), 1); + + assert.equal(account4EventBalance.toNumber(), 1); + assert.equal(account5EventBalance.toNumber(), 0); + }); + + it('should throw exception because event is not started yet', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(web3.eth.accounts[0], 1000); + + let balance1 = await this.token.getBalanceAtEventStart(0, web3.eth.accounts[0]).should.be.rejectedWith('revert'); + }); + + it('should work correctly if time passed and new event is started',async() => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account3, 100); + await this.token.mint(account4, 20); + + // 1 - create event 1 + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event == 'EventStarted'); + const eventID1 = events.filter(e => e.args._address == creator)[0].args._eventID; + + // 2 - transfer tokens + await this.token.transfer(account5, 5, {from: account3}); + await this.token.transfer(account5, 7, {from: account4}); + + assert.equal(await this.token.balanceOf(account3), 95); + assert.equal(await this.token.balanceOf(account4), 13); + assert.equal(await this.token.balanceOf(account5), 12); + + assert.equal(await this.token.getBalanceAtEventStart(eventID1, account3), 100); + assert.equal(await this.token.getBalanceAtEventStart(eventID1, account4), 20); + assert.equal(await this.token.getBalanceAtEventStart(eventID1, account5), 0); + + // 3 - finish event + await this.token.finishEvent(eventID1); + // 4 - increase time + let now = web3.eth.getBlock('latest').timestamp; + await increaseTimeTo(now + duration.seconds(1)); + + // 5 - create event 2 + const tx2 = await this.token.startNewEvent(); + const events2 = tx2.logs.filter(l => l.event == 'EventStarted'); + const eventID2 = events2.filter(e => e.args._address == creator)[0].args._eventID; + + // 6 - CHECK BALANCES + assert.equal(await this.token.balanceOf(account3), 95); + assert.equal(await this.token.balanceOf(account4), 13); + assert.equal(await this.token.balanceOf(account5), 12); + + assert.equal(await this.token.getBalanceAtEventStart(eventID2, account3), 95); + assert.equal(await this.token.getBalanceAtEventStart(eventID2, account4), 13); + assert.equal(await this.token.getBalanceAtEventStart(eventID2, account5), 12); + + // 7 - transfer tokens again + await this.token.transfer(account5, 2, {from: account3}); + await this.token.transfer(account5, 1, {from: account4}); + + // 8 - CHECK BALANCES again + assert.equal(await this.token.balanceOf(account3), 93); + assert.equal(await this.token.balanceOf(account4), 12); + assert.equal(await this.token.balanceOf(account5), 15); + + assert.equal(await this.token.getBalanceAtEventStart(eventID2, account3), 95); + assert.equal(await this.token.getBalanceAtEventStart(eventID2, account4), 13); + assert.equal(await this.token.getBalanceAtEventStart(eventID2, account5), 12); + + // 9 - finish event + await this.token.finishEvent(eventID2); + }); + + // That is a feature, not a bug! + it('should work correctly if time NOT passed and new event is started',async() => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account3, 100); + await this.token.mint(account4, 20); + + // 1 - create event 1 + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event == 'EventStarted'); + const eventID1 = events.filter(e => e.args._address == creator)[0].args._eventID; + + // 2 - transfer tokens + await this.token.transfer(account5, 5, {from: account3}); + await this.token.transfer(account5, 7, {from: account4}); + + assert.equal(await this.token.balanceOf(account3), 95); + assert.equal(await this.token.balanceOf(account4), 13); + assert.equal(await this.token.balanceOf(account5), 12); + + assert.equal(await this.token.getBalanceAtEventStart(eventID1, account3), 100); + assert.equal(await this.token.getBalanceAtEventStart(eventID1, account4), 20); + assert.equal(await this.token.getBalanceAtEventStart(eventID1, account5), 0); + + // 3 - finish event + await this.token.finishEvent(eventID1); + // 4 - DO NOT increase time!!! + // let now = web3.eth.getBlock('latest').timestamp; + //await increaseTimeTo(now + duration.seconds(1)); + + // 5 - create event 2 + const tx2 = await this.token.startNewEvent(); + const events2 = tx2.logs.filter(l => l.event == 'EventStarted'); + const eventID2 = events2.filter(e => e.args._address == creator)[0].args._eventID; + + // 6 - CHECK BALANCES + assert.equal(await this.token.balanceOf(account3), 95); + assert.equal(await this.token.balanceOf(account4), 13); + assert.equal(await this.token.balanceOf(account5), 12); + + // WARNING: + // We do not give STRONG guarantees. + // In case time has not passed - it will return 100 + // in case time HAS passed between the calls - it will return 95! + // + // We do not give STRONG guarantees. The return value is time-dependent: + // If startNewEvent() is called and then immediately getBalanceAtEventStart() -> it CAN return wrong data + // In case time between these calls has passed -> the return value is ALWAYS correct. + const balance = await this.token.getBalanceAtEventStart(eventID2, account3); + const isEqual = (balance==100) || (balance==95); + assert.equal(isEqual, true); + }); + }); + + describe('finishEvent', function () { + it('should not be possible to call by non-owner',async() => { + this.token = await PreserveBalancesOnTransferToken.new(); + + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event == 'EventStarted'); + const eventID = events.filter(e => e.args._address == creator)[0].args._eventID; + await this.token.finishEvent(eventID, {from: account3} ).should.be.rejectedWith('revert'); + }); + + it('should throw revert() if VotingID is wrong',async() => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account4, 1); + + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event == 'EventStarted'); + const eventID = events.filter(e => e.args._address == creator)[0].args._eventID; + + await this.token.transfer(account5, 1, {from: account4}); + + let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); + + assert.equal(account4EventBalance.toNumber(), 1); + assert.equal(account5EventBalance.toNumber(), 0); + + await this.token.finishEvent(75).should.be.rejectedWith('revert'); + }); + }); + }) From 6b65d2ef3769752e8d1a50881d7984a15ea40d28 Mon Sep 17 00:00:00 2001 From: Anthony Akentiev Date: Sat, 21 Jul 2018 14:43:47 +0300 Subject: [PATCH 02/10] Fixing linter errors/warnings --- .../crowdsale/emission/AllowanceCrowdsale.sol | 2 + contracts/mocks/RBACCappedTokenMock.sol | 1 + contracts/mocks/WhitelistMock.sol | 1 + .../ERC20/PreserveBalancesOnTransferToken.sol | 74 ++- .../PreserveBalancesOnTransferToken.test.js | 572 +++++++++--------- 5 files changed, 332 insertions(+), 318 deletions(-) diff --git a/contracts/crowdsale/emission/AllowanceCrowdsale.sol b/contracts/crowdsale/emission/AllowanceCrowdsale.sol index 7635ca7d81e..e271076a8a2 100644 --- a/contracts/crowdsale/emission/AllowanceCrowdsale.sol +++ b/contracts/crowdsale/emission/AllowanceCrowdsale.sol @@ -5,6 +5,8 @@ import "../../token/ERC20/ERC20.sol"; import "../../token/ERC20/ERC20Basic.sol"; import "../../token/ERC20/SafeERC20.sol"; import "../../math/SafeMath.sol"; + + /** * @title AllowanceCrowdsale * @dev Extension of Crowdsale where tokens are held by a wallet, which approves an allowance to the crowdsale. diff --git a/contracts/mocks/RBACCappedTokenMock.sol b/contracts/mocks/RBACCappedTokenMock.sol index 9de98a893d9..d14116dd340 100644 --- a/contracts/mocks/RBACCappedTokenMock.sol +++ b/contracts/mocks/RBACCappedTokenMock.sol @@ -3,6 +3,7 @@ pragma solidity ^0.4.24; import "../token/ERC20/RBACMintableToken.sol"; import "../token/ERC20/CappedToken.sol"; + contract RBACCappedTokenMock is CappedToken, RBACMintableToken { constructor( uint256 _cap diff --git a/contracts/mocks/WhitelistMock.sol b/contracts/mocks/WhitelistMock.sol index d6c2eef8e03..d4d8e5a3e8b 100644 --- a/contracts/mocks/WhitelistMock.sol +++ b/contracts/mocks/WhitelistMock.sol @@ -2,6 +2,7 @@ pragma solidity ^0.4.24; import "../access/Whitelist.sol"; + contract WhitelistMock is Whitelist { function onlyWhitelistedCanDoThis() diff --git a/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol b/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol index 7a7f19a9ff9..19712c1f3a9 100644 --- a/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol +++ b/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol @@ -3,6 +3,7 @@ pragma solidity ^0.4.24; import "./MintableToken.sol"; import "./BurnableToken.sol"; + /** * @title PreserveBalancesOnTransferToken (Copy-on-Write) token * @author Based on code by Thetta DAO Framework: https://github.com/Thetta/Thetta-DAO-Framework/ @@ -27,15 +28,14 @@ import "./BurnableToken.sol"; */ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { struct Holder { - uint256 balance; - uint lastUpdateTime; - } + uint256 balance; + uint lastUpdateTime; + } struct Event { - mapping (address => Holder) holders; - - bool isEventInProgress; - uint eventStartTime; + mapping (address => Holder) holders; + bool isEventInProgress; + uint eventStartTime; } mapping (uint => Event) events; @@ -60,7 +60,9 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { * @param _to address The address which you want to transfer to * @param _value uint256 the amount of tokens to be transferred */ - function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { + function transferFrom(address _from, address _to, uint256 _value) + public returns (bool) + { updateCopyOnWriteMaps(_from, _to); return super.transferFrom(_from, _to, _value); } @@ -72,29 +74,22 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { * @param _amount The amount of tokens to mint. * @return A boolean that indicates if the operation was successful. */ - function mint(address _to, uint256 _amount) canMint onlyOwner public returns(bool){ + function mint(address _to, uint256 _amount) canMint onlyOwner + public returns(bool) + { updateCopyOnWriteMap(_to); return super.mint(_to, _amount); } -// BurnableToken overrides: - /** @dev This is an override of internal method! Public method burn() calls _burn() automatically - * (see BurnableToken implementation) - */ - function _burn(address _who, uint256 _value) internal { - updateCopyOnWriteMap(_who); - super._burn(_who, _value); - } - // PreserveBalancesOnTransferToken - new methods: /** * @dev Function to signal that some event happens (dividends are calculated, voting, etc) - * so we need to start preserving balances AT THE time this event happened. + * so we need to start preserving balances AT THE time this event happened. * @return An index of the event started. */ - function startNewEvent() public onlyOwner returns(uint){ - for(uint i = 0; i < 20; i++){ - if(!events[i].isEventInProgress){ + function startNewEvent() public onlyOwner returns(uint) { + for (uint i = 0; i < 20; i++) { + if (!events[i].isEventInProgress) { events[i].isEventInProgress = true; events[i].eventStartTime = now; @@ -120,22 +115,33 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { * @dev Returns the balance of the address _for at the time event _eventID happened * !!! WARNING !!! * We do not give STRONG guarantees. The return value is time-dependent: - * If startNewEvent() is called and then immediately getBalanceAtEventStart() -> it CAN return wrong data + * If startNewEvent() and then getBalanceAtEventStart() -> it CAN return wrong data * In case time between these calls has passed -> the return value is ALWAYS correct. * * Please see tests. * return Token balance (when the event started, but not a CURRENT balanceOf()!) */ - function getBalanceAtEventStart(uint _eventID, address _for) public view returns(uint256) { + function getBalanceAtEventStart(uint _eventID, address _for) + public view returns(uint256) + { require(events[_eventID].isEventInProgress); - if(!isBalanceWasChangedAfterEventStarted(_eventID, _for)){ + if (!isBalanceWasChangedAfterEventStarted(_eventID, _for)) { return balances[_for]; } return events[_eventID].holders[_for].balance; } +// BurnableToken overrides: + /** @dev This is an override of internal method! Public method burn() calls _burn() automatically + * (see BurnableToken implementation) + */ + function _burn(address _who, uint256 _value) internal { + updateCopyOnWriteMap(_who); + super._burn(_who, _value); + } + // Internal methods: function updateCopyOnWriteMaps(address _from, address _to) internal { updateCopyOnWriteMap(_to); @@ -143,20 +149,26 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { } function updateCopyOnWriteMap(address _for) internal { - for(uint i = 0; i < 20; i++){ + for (uint i = 0; i < 20; i++) { bool res = isNeedToUpdateBalancesMap(i, _for); - if(res){ + if (res) { events[i].holders[_for].balance = balances[_for]; events[i].holders[_for].lastUpdateTime = now; } } } - function isNeedToUpdateBalancesMap(uint _eventID, address _for) internal view returns(bool) { - return events[_eventID].isEventInProgress && !isBalanceWasChangedAfterEventStarted(_eventID, _for); + function isNeedToUpdateBalancesMap(uint _eventID, address _for) + internal view returns(bool) + { + return (events[_eventID].isEventInProgress && + !isBalanceWasChangedAfterEventStarted(_eventID, _for)); } - function isBalanceWasChangedAfterEventStarted(uint _eventID, address _for) internal view returns(bool){ - return (events[_eventID].holders[_for].lastUpdateTime >= events[_eventID].eventStartTime); + function isBalanceWasChangedAfterEventStarted(uint _eventID, address _for) + internal view returns(bool) + { + return (events[_eventID].holders[_for].lastUpdateTime >= + events[_eventID].eventStartTime); } } diff --git a/test/token/ERC20/PreserveBalancesOnTransferToken.test.js b/test/token/ERC20/PreserveBalancesOnTransferToken.test.js index e6a0399e72f..72106b1cee2 100644 --- a/test/token/ERC20/PreserveBalancesOnTransferToken.test.js +++ b/test/token/ERC20/PreserveBalancesOnTransferToken.test.js @@ -25,7 +25,6 @@ function increaseTime (duration) { }); } - function increaseTimeTo (target) { let now = web3.eth.getBlock('latest').timestamp; if (target < now) throw Error(`Cannot increase current time(${now}) to a moment in the past(${target})`); @@ -42,359 +41,358 @@ const duration = { years: function (val) { return val * this.days(365); }, }; - require('chai') .use(require('chai-as-promised')) .use(require('chai-bignumber')(BigNumber)) .should(); - contract('PreserveBalancesOnTransferToken', (accounts) => { - const creator = accounts[0]; - const account3 = accounts[3]; - const account4 = accounts[4]; - const account5 = accounts[5]; +contract('PreserveBalancesOnTransferToken', (accounts) => { + const creator = accounts[0]; + const account3 = accounts[3]; + const account4 = accounts[4]; + const account5 = accounts[5]; - beforeEach(async function () { + beforeEach(async function () { - }); + }); - describe('mint', function () { - it('should fail due to not owner call', async function () { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.mint(web3.eth.accounts[1], 1000, {from: web3.eth.accounts[1]}).should.be.rejectedWith('revert'); - }); + describe('mint', function () { + it('should fail due to not owner call', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(web3.eth.accounts[1], 1000, + { from: web3.eth.accounts[1] }).should.be.rejectedWith('revert'); + }); - it('should fail with isMintable = false', async function () { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.mint(web3.eth.accounts[1], 1000); - }); + it('should fail with isMintable = false', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(web3.eth.accounts[1], 1000); + }); - it('should fail due to finishMinting() call', async function () { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.finishMinting(); - await this.token.mint(web3.eth.accounts[1], 1000).should.be.rejectedWith('revert'); - }); + it('should fail due to finishMinting() call', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.finishMinting(); + await this.token.mint(web3.eth.accounts[1], 1000).should.be.rejectedWith('revert'); + }); - it('should pass', async function () { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.mint(web3.eth.accounts[0], 1000); - let balance = await this.token.balanceOf(web3.eth.accounts[0]); - assert.equal(balance.toNumber(), 1000); - }); + it('should pass', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(web3.eth.accounts[0], 1000); + let balance = await this.token.balanceOf(web3.eth.accounts[0]); + assert.equal(balance.toNumber(), 1000); }); + }); - describe('burn', function () { - it('should fail due to not owner call', async function () { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.mint(web3.eth.accounts[1], 1000); - await this.token.burn(1000, {from: web3.eth.accounts[0]}).should.be.rejectedWith('revert'); - }); + describe('burn', function () { + it('should fail due to not owner call', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(web3.eth.accounts[1], 1000); + await this.token.burn(1000, { from: web3.eth.accounts[0] }).should.be.rejectedWith('revert'); + }); - it('should fail due to not enough tokens in the address provided', async function () { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.burn(1000).should.be.rejectedWith('revert'); - }); + it('should fail due to not enough tokens in the address provided', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.burn(1000).should.be.rejectedWith('revert'); + }); - it('should pass', async function () { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.mint(web3.eth.accounts[0], 1000); - await this.token.burn(1000); - let balance = await this.token.balanceOf(web3.eth.accounts[0]); - assert.equal(balance.toNumber(), 0); - }); + it('should pass', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(web3.eth.accounts[0], 1000); + await this.token.burn(1000); + let balance = await this.token.balanceOf(web3.eth.accounts[0]); + assert.equal(balance.toNumber(), 0); }); + }); - describe('startNewEvent', function() { - it('should not allow to create > 20 separate events',async() => { - this.token = await PreserveBalancesOnTransferToken.new(); - - await this.token.startNewEvent();//1 - await this.token.startNewEvent();//2 - await this.token.startNewEvent();//3 - await this.token.startNewEvent();//4 - await this.token.startNewEvent();//5 - await this.token.startNewEvent();//6 - await this.token.startNewEvent();//7 - await this.token.startNewEvent();//8 - await this.token.startNewEvent();//9 - await this.token.startNewEvent();//10 - await this.token.startNewEvent();//11 - await this.token.startNewEvent();//12 - await this.token.startNewEvent();//13 - await this.token.startNewEvent();//14 - await this.token.startNewEvent();//15 - await this.token.startNewEvent();//16 - await this.token.startNewEvent();//17 - await this.token.startNewEvent();//18 - await this.token.startNewEvent();//19 - await this.token.startNewEvent();//20 - await this.token.startNewEvent().should.be.rejectedWith('revert'); - }); + describe('startNewEvent', function () { + it('should not allow to create > 20 separate events', async () => { + this.token = await PreserveBalancesOnTransferToken.new(); + + await this.token.startNewEvent();// 1 + await this.token.startNewEvent();// 2 + await this.token.startNewEvent();// 3 + await this.token.startNewEvent();// 4 + await this.token.startNewEvent();// 5 + await this.token.startNewEvent();// 6 + await this.token.startNewEvent();// 7 + await this.token.startNewEvent();// 8 + await this.token.startNewEvent();// 9 + await this.token.startNewEvent();// 10 + await this.token.startNewEvent();// 11 + await this.token.startNewEvent();// 12 + await this.token.startNewEvent();// 13 + await this.token.startNewEvent();// 14 + await this.token.startNewEvent();// 15 + await this.token.startNewEvent();// 16 + await this.token.startNewEvent();// 17 + await this.token.startNewEvent();// 18 + await this.token.startNewEvent();// 19 + await this.token.startNewEvent();// 20 + await this.token.startNewEvent().should.be.rejectedWith('revert'); + }); - it('should not be possible to call by non-owner',async() => { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.startNewEvent({from: account3}).should.be.rejectedWith('revert'); - }); + it('should not be possible to call by non-owner', async () => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.startNewEvent({ from: account3 }).should.be.rejectedWith('revert'); }); + }); - describe('getBalanceAtEventStart', function () { - it('should preserve balances if no transfers happened after event is started',async() => { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.mint(account4, 1); + describe('getBalanceAtEventStart', function () { + it('should preserve balances if no transfers happened after event is started', async () => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account4, 1); - let account4Balance = await this.token.balanceOf(account4); - let account5Balance = await this.token.balanceOf(account5); + let account4Balance = await this.token.balanceOf(account4); + let account5Balance = await this.token.balanceOf(account5); - assert.equal(account4Balance.toNumber(), 1); - assert.equal(account5Balance.toNumber(), 0); + assert.equal(account4Balance.toNumber(), 1); + assert.equal(account5Balance.toNumber(), 0); - const tx = await this.token.startNewEvent(); - const events = tx.logs.filter(l => l.event == 'EventStarted'); - const eventID = events.filter(e => e.args._address == creator)[0].args._eventID; + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event === 'EventStarted'); + const eventID = events.filter(e => e.args._address === creator)[0].args._eventID; - let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); - let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); + let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); - assert.equal(account4EventBalance.toNumber(), 1); - assert.equal(account5EventBalance.toNumber(), 0); - }); + assert.equal(account4EventBalance.toNumber(), 1); + assert.equal(account5EventBalance.toNumber(), 0); + }); - it('should preserve balances after event is started',async() => { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.mint(account4, 1); + it('should preserve balances after event is started', async () => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account4, 1); - const tx = await this.token.startNewEvent(); - const events = tx.logs.filter(l => l.event == 'EventStarted'); - const eventID = events.filter(e => e.args._address == creator)[0].args._eventID; + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event === 'EventStarted'); + const eventID = events.filter(e => e.args._address === creator)[0].args._eventID; - await this.token.transfer(account5, 1, {from: account4}); + await this.token.transfer(account5, 1, { from: account4 }); - let account4Balance = await this.token.balanceOf(account4); - let account5Balance = await this.token.balanceOf(account5); + let account4Balance = await this.token.balanceOf(account4); + let account5Balance = await this.token.balanceOf(account5); - let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); - let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); + let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); - assert.equal(account4Balance.toNumber(), 0); - assert.equal(account5Balance.toNumber(), 1); + assert.equal(account4Balance.toNumber(), 0); + assert.equal(account5Balance.toNumber(), 1); - assert.equal(account4EventBalance.toNumber(), 1); - assert.equal(account5EventBalance.toNumber(), 0); - }); + assert.equal(account4EventBalance.toNumber(), 1); + assert.equal(account5EventBalance.toNumber(), 0); + }); - it('should preserve balances after event is started and mint called',async() => { - this.token = await PreserveBalancesOnTransferToken.new(); + it('should preserve balances after event is started and mint called', async () => { + this.token = await PreserveBalancesOnTransferToken.new(); - const tx = await this.token.startNewEvent(); - const events = tx.logs.filter(l => l.event == 'EventStarted'); - const eventID = events.filter(e => e.args._address == creator)[0].args._eventID; + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event === 'EventStarted'); + const eventID = events.filter(e => e.args._address === creator)[0].args._eventID; - await this.token.mint(account4, 1); + await this.token.mint(account4, 1); - let account4Balance = await this.token.balanceOf(account4); - let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + let account4Balance = await this.token.balanceOf(account4); + let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); - assert.equal(account4Balance.toNumber(), 1); - assert.equal(account4EventBalance.toNumber(), 0); - }); + assert.equal(account4Balance.toNumber(), 1); + assert.equal(account4EventBalance.toNumber(), 0); + }); - it('should throw exception when trying to check balancesAtVoting after event is ended',async() => { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.mint(account4, 1); + it('should throw exception when trying to check balancesAtVoting after event is ended', async () => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account4, 1); - const tx = await this.token.startNewEvent(); - const events = tx.logs.filter(l => l.event == 'EventStarted'); - const eventID = events.filter(e => e.args._address == creator)[0].args._eventID; + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event === 'EventStarted'); + const eventID = events.filter(e => e.args._address === creator)[0].args._eventID; - await this.token.transfer(account5, 1, {from: account4}); + await this.token.transfer(account5, 1, { from: account4 }); - let account4Balance = await this.token.balanceOf(account4); - let account5Balance = await this.token.balanceOf(account5); + let account4Balance = await this.token.balanceOf(account4); + let account5Balance = await this.token.balanceOf(account5); - assert.equal(account4Balance.toNumber(), 0); - assert.equal(account5Balance.toNumber(), 1); + assert.equal(account4Balance.toNumber(), 0); + assert.equal(account5Balance.toNumber(), 1); - await this.token.finishEvent(eventID); + await this.token.finishEvent(eventID); - account4Balance = await this.token.balanceOf(account4); - account5Balance = await this.token.balanceOf(account5); + account4Balance = await this.token.balanceOf(account4); + account5Balance = await this.token.balanceOf(account5); - assert.equal(account4Balance.toNumber(), 0); - assert.equal(account5Balance.toNumber(), 1); + assert.equal(account4Balance.toNumber(), 0); + assert.equal(account5Balance.toNumber(), 1); - let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4).should.be.rejectedWith('revert'); - }); + await this.token.getBalanceAtEventStart(eventID, account4).should.be.rejectedWith('revert'); + }); - it('should preserve balances after event is started and transferFrom is called',async() => { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.mint(account4, 1); + it('should preserve balances after event is started and transferFrom is called', async () => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account4, 1); - const tx = await this.token.startNewEvent(); - const events = tx.logs.filter(l => l.event == 'EventStarted'); - const eventID = events.filter(e => e.args._address == creator)[0].args._eventID; + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event === 'EventStarted'); + const eventID = events.filter(e => e.args._address === creator)[0].args._eventID; - await this.token.approve(account3, 1, {from: account4}); - await this.token.transferFrom(account4, account5, 1, {from: account3}); + await this.token.approve(account3, 1, { from: account4 }); + await this.token.transferFrom(account4, account5, 1, { from: account3 }); - let account4Balance = await this.token.balanceOf(account4); - let account5Balance = await this.token.balanceOf(account5); + let account4Balance = await this.token.balanceOf(account4); + let account5Balance = await this.token.balanceOf(account5); - let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); - let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); + let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); - assert.equal(account4Balance.toNumber(), 0); - assert.equal(account5Balance.toNumber(), 1); + assert.equal(account4Balance.toNumber(), 0); + assert.equal(account5Balance.toNumber(), 1); - assert.equal(account4EventBalance.toNumber(), 1); - assert.equal(account5EventBalance.toNumber(), 0); - }); + assert.equal(account4EventBalance.toNumber(), 1); + assert.equal(account5EventBalance.toNumber(), 0); + }); - it('should throw exception because event is not started yet', async function () { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.mint(web3.eth.accounts[0], 1000); - - let balance1 = await this.token.getBalanceAtEventStart(0, web3.eth.accounts[0]).should.be.rejectedWith('revert'); - }); + it('should throw exception because event is not started yet', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(web3.eth.accounts[0], 1000); + await this.token.getBalanceAtEventStart(0, web3.eth.accounts[0]).should.be.rejectedWith('revert'); + }); - it('should work correctly if time passed and new event is started',async() => { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.mint(account3, 100); - await this.token.mint(account4, 20); - - // 1 - create event 1 - const tx = await this.token.startNewEvent(); - const events = tx.logs.filter(l => l.event == 'EventStarted'); - const eventID1 = events.filter(e => e.args._address == creator)[0].args._eventID; - - // 2 - transfer tokens - await this.token.transfer(account5, 5, {from: account3}); - await this.token.transfer(account5, 7, {from: account4}); - - assert.equal(await this.token.balanceOf(account3), 95); - assert.equal(await this.token.balanceOf(account4), 13); - assert.equal(await this.token.balanceOf(account5), 12); - - assert.equal(await this.token.getBalanceAtEventStart(eventID1, account3), 100); - assert.equal(await this.token.getBalanceAtEventStart(eventID1, account4), 20); - assert.equal(await this.token.getBalanceAtEventStart(eventID1, account5), 0); - - // 3 - finish event - await this.token.finishEvent(eventID1); - // 4 - increase time - let now = web3.eth.getBlock('latest').timestamp; - await increaseTimeTo(now + duration.seconds(1)); - - // 5 - create event 2 - const tx2 = await this.token.startNewEvent(); - const events2 = tx2.logs.filter(l => l.event == 'EventStarted'); - const eventID2 = events2.filter(e => e.args._address == creator)[0].args._eventID; - - // 6 - CHECK BALANCES - assert.equal(await this.token.balanceOf(account3), 95); - assert.equal(await this.token.balanceOf(account4), 13); - assert.equal(await this.token.balanceOf(account5), 12); - - assert.equal(await this.token.getBalanceAtEventStart(eventID2, account3), 95); - assert.equal(await this.token.getBalanceAtEventStart(eventID2, account4), 13); - assert.equal(await this.token.getBalanceAtEventStart(eventID2, account5), 12); - - // 7 - transfer tokens again - await this.token.transfer(account5, 2, {from: account3}); - await this.token.transfer(account5, 1, {from: account4}); - - // 8 - CHECK BALANCES again - assert.equal(await this.token.balanceOf(account3), 93); - assert.equal(await this.token.balanceOf(account4), 12); - assert.equal(await this.token.balanceOf(account5), 15); - - assert.equal(await this.token.getBalanceAtEventStart(eventID2, account3), 95); - assert.equal(await this.token.getBalanceAtEventStart(eventID2, account4), 13); - assert.equal(await this.token.getBalanceAtEventStart(eventID2, account5), 12); - - // 9 - finish event - await this.token.finishEvent(eventID2); - }); + it('should work correctly if time passed and new event is started', async () => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account3, 100); + await this.token.mint(account4, 20); + + // 1 - create event 1 + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event === 'EventStarted'); + const eventID1 = events.filter(e => e.args._address === creator)[0].args._eventID; + + // 2 - transfer tokens + await this.token.transfer(account5, 5, { from: account3 }); + await this.token.transfer(account5, 7, { from: account4 }); + + assert.equal(await this.token.balanceOf(account3), 95); + assert.equal(await this.token.balanceOf(account4), 13); + assert.equal(await this.token.balanceOf(account5), 12); + + assert.equal(await this.token.getBalanceAtEventStart(eventID1, account3), 100); + assert.equal(await this.token.getBalanceAtEventStart(eventID1, account4), 20); + assert.equal(await this.token.getBalanceAtEventStart(eventID1, account5), 0); + + // 3 - finish event + await this.token.finishEvent(eventID1); + // 4 - increase time + let now = web3.eth.getBlock('latest').timestamp; + await increaseTimeTo(now + duration.seconds(1)); + + // 5 - create event 2 + const tx2 = await this.token.startNewEvent(); + const events2 = tx2.logs.filter(l => l.event === 'EventStarted'); + const eventID2 = events2.filter(e => e.args._address === creator)[0].args._eventID; + + // 6 - CHECK BALANCES + assert.equal(await this.token.balanceOf(account3), 95); + assert.equal(await this.token.balanceOf(account4), 13); + assert.equal(await this.token.balanceOf(account5), 12); + + assert.equal(await this.token.getBalanceAtEventStart(eventID2, account3), 95); + assert.equal(await this.token.getBalanceAtEventStart(eventID2, account4), 13); + assert.equal(await this.token.getBalanceAtEventStart(eventID2, account5), 12); + + // 7 - transfer tokens again + await this.token.transfer(account5, 2, { from: account3 }); + await this.token.transfer(account5, 1, { from: account4 }); + + // 8 - CHECK BALANCES again + assert.equal(await this.token.balanceOf(account3), 93); + assert.equal(await this.token.balanceOf(account4), 12); + assert.equal(await this.token.balanceOf(account5), 15); + + assert.equal(await this.token.getBalanceAtEventStart(eventID2, account3), 95); + assert.equal(await this.token.getBalanceAtEventStart(eventID2, account4), 13); + assert.equal(await this.token.getBalanceAtEventStart(eventID2, account5), 12); + + // 9 - finish event + await this.token.finishEvent(eventID2); + }); - // That is a feature, not a bug! - it('should work correctly if time NOT passed and new event is started',async() => { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.mint(account3, 100); - await this.token.mint(account4, 20); + // That is a feature, not a bug! + it('should work correctly if time NOT passed and new event is started', async () => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account3, 100); + await this.token.mint(account4, 20); - // 1 - create event 1 - const tx = await this.token.startNewEvent(); - const events = tx.logs.filter(l => l.event == 'EventStarted'); - const eventID1 = events.filter(e => e.args._address == creator)[0].args._eventID; + // 1 - create event 1 + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event === 'EventStarted'); + const eventID1 = events.filter(e => e.args._address === creator)[0].args._eventID; - // 2 - transfer tokens - await this.token.transfer(account5, 5, {from: account3}); - await this.token.transfer(account5, 7, {from: account4}); + // 2 - transfer tokens + await this.token.transfer(account5, 5, { from: account3 }); + await this.token.transfer(account5, 7, { from: account4 }); - assert.equal(await this.token.balanceOf(account3), 95); - assert.equal(await this.token.balanceOf(account4), 13); - assert.equal(await this.token.balanceOf(account5), 12); + assert.equal(await this.token.balanceOf(account3), 95); + assert.equal(await this.token.balanceOf(account4), 13); + assert.equal(await this.token.balanceOf(account5), 12); - assert.equal(await this.token.getBalanceAtEventStart(eventID1, account3), 100); - assert.equal(await this.token.getBalanceAtEventStart(eventID1, account4), 20); - assert.equal(await this.token.getBalanceAtEventStart(eventID1, account5), 0); - - // 3 - finish event - await this.token.finishEvent(eventID1); - // 4 - DO NOT increase time!!! - // let now = web3.eth.getBlock('latest').timestamp; - //await increaseTimeTo(now + duration.seconds(1)); - - // 5 - create event 2 - const tx2 = await this.token.startNewEvent(); - const events2 = tx2.logs.filter(l => l.event == 'EventStarted'); - const eventID2 = events2.filter(e => e.args._address == creator)[0].args._eventID; - - // 6 - CHECK BALANCES - assert.equal(await this.token.balanceOf(account3), 95); - assert.equal(await this.token.balanceOf(account4), 13); - assert.equal(await this.token.balanceOf(account5), 12); - - // WARNING: - // We do not give STRONG guarantees. - // In case time has not passed - it will return 100 - // in case time HAS passed between the calls - it will return 95! - // - // We do not give STRONG guarantees. The return value is time-dependent: - // If startNewEvent() is called and then immediately getBalanceAtEventStart() -> it CAN return wrong data - // In case time between these calls has passed -> the return value is ALWAYS correct. - const balance = await this.token.getBalanceAtEventStart(eventID2, account3); - const isEqual = (balance==100) || (balance==95); - assert.equal(isEqual, true); - }); + assert.equal(await this.token.getBalanceAtEventStart(eventID1, account3), 100); + assert.equal(await this.token.getBalanceAtEventStart(eventID1, account4), 20); + assert.equal(await this.token.getBalanceAtEventStart(eventID1, account5), 0); + + // 3 - finish event + await this.token.finishEvent(eventID1); + // 4 - DO NOT increase time!!! + // let now = web3.eth.getBlock('latest').timestamp; + // await increaseTimeTo(now + duration.seconds(1)); + + // 5 - create event 2 + const tx2 = await this.token.startNewEvent(); + const events2 = tx2.logs.filter(l => l.event === 'EventStarted'); + const eventID2 = events2.filter(e => e.args._address === creator)[0].args._eventID; + + // 6 - CHECK BALANCES + assert.equal(await this.token.balanceOf(account3), 95); + assert.equal(await this.token.balanceOf(account4), 13); + assert.equal(await this.token.balanceOf(account5), 12); + + // WARNING: + // We do not give STRONG guarantees. + // In case time has not passed - it will return 100 + // in case time HAS passed between the calls - it will return 95! + // + // We do not give STRONG guarantees. The return value is time-dependent: + // If startNewEvent() is called and then immediately getBalanceAtEventStart() -> it CAN return wrong data + // In case time between these calls has passed -> the return value is ALWAYS correct. + const balance = await this.token.getBalanceAtEventStart(eventID2, account3); + const isEqual = (balance.toNumber() === 100) || (balance.toNumber() === 95); + assert.equal(isEqual, true); }); + }); - describe('finishEvent', function () { - it('should not be possible to call by non-owner',async() => { - this.token = await PreserveBalancesOnTransferToken.new(); + describe('finishEvent', function () { + it('should not be possible to call by non-owner', async () => { + this.token = await PreserveBalancesOnTransferToken.new(); - const tx = await this.token.startNewEvent(); - const events = tx.logs.filter(l => l.event == 'EventStarted'); - const eventID = events.filter(e => e.args._address == creator)[0].args._eventID; - await this.token.finishEvent(eventID, {from: account3} ).should.be.rejectedWith('revert'); - }); + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event === 'EventStarted'); + const eventID = events.filter(e => e.args._address === creator)[0].args._eventID; + await this.token.finishEvent(eventID, { from: account3 }).should.be.rejectedWith('revert'); + }); - it('should throw revert() if VotingID is wrong',async() => { - this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.mint(account4, 1); + it('should throw revert() if VotingID is wrong', async () => { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account4, 1); - const tx = await this.token.startNewEvent(); - const events = tx.logs.filter(l => l.event == 'EventStarted'); - const eventID = events.filter(e => e.args._address == creator)[0].args._eventID; + const tx = await this.token.startNewEvent(); + const events = tx.logs.filter(l => l.event === 'EventStarted'); + const eventID = events.filter(e => e.args._address === creator)[0].args._eventID; - await this.token.transfer(account5, 1, {from: account4}); + await this.token.transfer(account5, 1, { from: account4 }); - let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); - let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); + let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); - assert.equal(account4EventBalance.toNumber(), 1); - assert.equal(account5EventBalance.toNumber(), 0); + assert.equal(account4EventBalance.toNumber(), 1); + assert.equal(account5EventBalance.toNumber(), 0); - await this.token.finishEvent(75).should.be.rejectedWith('revert'); - }); + await this.token.finishEvent(75).should.be.rejectedWith('revert'); }); - }) + }); +}); From 81592145eadc46b6ea8e17af9b8e7e40f03f07f4 Mon Sep 17 00:00:00 2001 From: Anthony Akentiev Date: Tue, 31 Jul 2018 19:18:05 +0300 Subject: [PATCH 03/10] Adding SnapshotToken + tests --- .../PreserveBalancesOnTransferTokenMock.sol | 25 +++ .../ERC20/PreserveBalancesOnTransferToken.sol | 145 +++++++++++++- .../PreserveBalancesOnTransferToken.test.js | 187 +++++++++++++++++- 3 files changed, 351 insertions(+), 6 deletions(-) create mode 100644 contracts/mocks/PreserveBalancesOnTransferTokenMock.sol diff --git a/contracts/mocks/PreserveBalancesOnTransferTokenMock.sol b/contracts/mocks/PreserveBalancesOnTransferTokenMock.sol new file mode 100644 index 00000000000..376b511a228 --- /dev/null +++ b/contracts/mocks/PreserveBalancesOnTransferTokenMock.sol @@ -0,0 +1,25 @@ +pragma solidity ^0.4.24; + +import "../token/ERC20/PreserveBalancesOnTransferToken.sol"; + + +contract PreserveBalancesOnTransferTokenMock is PreserveBalancesOnTransferToken { + // without modifiers. allow anyone to call this + function startNewEvent() public returns(uint) { + // do nothing. just for tests + return 0; + } + + // without modifiers. allow anyone to call this + function finishEvent(uint _eventID) public { + // do nothing. just for tests + } + + function TEST_callStartForSnapshot(SnapshotToken _st) public { + _st.start(); + } + + function TEST_callFinishForSnapshot(SnapshotToken _st) public { + _st.finish(); + } +} diff --git a/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol b/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol index 19712c1f3a9..856971e4589 100644 --- a/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol +++ b/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol @@ -10,7 +10,27 @@ import "./BurnableToken.sol"; * @dev Token that can preserve the balances after some EVENT happens (voting is started, didivends are calculated, etc) * without blocking the transfers! Please notice that EVENT in this case has nothing to do with Ethereum events. * - * Example of usage (pseudocode): + * Example of usage1 (pseudocode): + * PreserveBalancesOnTransferToken token; + * + * token.mint(ADDRESS_A, 100); + * assert.equal(token.balanceOf(ADDRESS_A), 100); + * assert.equal(token.balanceOf(ADDRESS_B), 0); + * + * SnapshotToken snapshot = token.createNewSnapshot(); + * token.transfer(ADDRESS_A, ADDRESS_B, 30); + * + * assert.equal(token.balanceOf(ADDRESS_A), 70); + * assert.equal(token.balanceOf(ADDRESS_B), 30); + * + * assert.equal(snapshot.balanceOf(ADDRESS_A), 100); + * assert.equal(snapshot.balanceOf(ADDRESS_B), 0); + * + * token.stopSnapshot(snapshot); + * + * Example of usage2 (pseudocode): + * PreserveBalancesOnTransferToken token; + * * token.mint(ADDRESS_A, 100); * assert.equal(token.balanceOf(ADDRESS_A), 100); * assert.equal(token.balanceOf(ADDRESS_B), 0); @@ -25,7 +45,7 @@ import "./BurnableToken.sol"; * assert.equal(token.getBalanceAtEvent(someEventID_1, ADDRESS_B), 0); * * token.finishEvent(someEventID_1); - */ +*/ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { struct Holder { uint256 balance; @@ -38,9 +58,16 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { uint eventStartTime; } mapping (uint => Event) events; + SnapshotToken[] snapshotTokens; event EventStarted(address indexed _address, uint _eventID); event EventFinished(address indexed _address, uint _eventID); + event SnapshotCreated(address indexed _snapshotTokenAddress); + + modifier onlyFromSnapshotOrOwner() { + require((msg.sender == owner) || isFromSnapshot(msg.sender)); + _; + } // BasicToken overrides: /** @@ -82,12 +109,39 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { } // PreserveBalancesOnTransferToken - new methods: + /** + * @dev Creates new ERC20 balances snapshot. + * In this case SnapshotToken is an easy way to get the balances + * using the standard 'balanceOf' method instead of getBalanceAtEventStart() + * @return Address of the new created snapshot ERC20 token. + */ + function createNewSnapshot() public onlyOwner returns(address) { + SnapshotToken st = new SnapshotToken(this); + + snapshotTokens.push(st); + // will call back this.startNewEvent(); + st.start(); + + emit SnapshotCreated(st); + return st; + } + + /** + * @dev End working with the ERC20 balances snapshot + * @param _st The SnapshotToken that was created with 'createNewSnapshot' + * method before + */ + function stopSnapshot(SnapshotToken _st) public onlyOwner { + // will call back this.finishEvent(); + _st.finish(); + } + /** * @dev Function to signal that some event happens (dividends are calculated, voting, etc) - * so we need to start preserving balances AT THE time this event happened. + * so we need to start preserving balances AT THE time this event happened. * @return An index of the event started. */ - function startNewEvent() public onlyOwner returns(uint) { + function startNewEvent() public onlyFromSnapshotOrOwner returns(uint) { for (uint i = 0; i < 20; i++) { if (!events[i].isEventInProgress) { events[i].isEventInProgress = true; @@ -104,7 +158,7 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { * @dev Function to signal that some event is finished * @param _eventID An index of the event that was previously returned by startNewEvent(). */ - function finishEvent(uint _eventID) public onlyOwner { + function finishEvent(uint _eventID) public onlyFromSnapshotOrOwner { require(events[_eventID].isEventInProgress); events[_eventID].isEventInProgress = false; @@ -171,4 +225,85 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { return (events[_eventID].holders[_for].lastUpdateTime >= events[_eventID].eventStartTime); } + + function isFromSnapshot(address _a) internal view returns(bool){ + for(uint i = 0; i < snapshotTokens.length; ++i) { + if(snapshotTokens[i] == _a) { + return true; + } + } + return false; + } +} + +/** + * @title SnapshotToken + * @author Based on code by Thetta DAO Framework: https://github.com/Thetta/Thetta-DAO-Framework/ + * @dev Wapper to use snapshot.balanceOf() instead of token.getBalanceAtEventStart() + * Should not be created directly. Please use PreserveBalancesOnTransferToken.createNewSnapshot() method +*/ +contract SnapshotToken is StandardToken, Ownable { + PreserveBalancesOnTransferToken public pbott; + uint public snapshotID = 0; + bool isStarted = false; + + constructor(PreserveBalancesOnTransferToken _pbott) public { + pbott = _pbott; + } + +// BasicToken overrides: + /** + * @dev Gets the balance of the specified address. + * @param _owner The address to query the the balance of. + * @return An uint256 representing the amount owned by the passed address. + */ + function balanceOf(address _owner) public view returns (uint256) { + return pbott.getBalanceAtEventStart(snapshotID, _owner); + } + + /** + * @dev Transfer token for a specified address. Blocked! + * @param _to The address to transfer to. + * @param _value The amount to be transferred. + */ + function transfer(address _to, uint256 _value) public returns (bool) { + revert(); + return false; + } + +// StandardToken overrides: + /** + * @dev Transfer tokens from one address to another. Blocked! + * @param _from address The address which you want to send tokens from + * @param _to address The address which you want to transfer to + * @param _value uint256 the amount of tokens to be transferred + */ + function transferFrom(address _from, address _to, uint256 _value) + public returns (bool) + { + revert(); + return false; + } + +// New methods: + /** + * @dev Should be called automatically from the PreserveBalancesOnTransferToken + */ + function start() public { + require(pbott == msg.sender); + require(!isStarted); + + snapshotID = pbott.startNewEvent(); + isStarted = true; + } + + /** + * @dev Should be called automatically from the PreserveBalancesOnTransferToken + */ + function finish() public { + require(pbott == msg.sender); + require(isStarted); + + pbott.finishEvent(snapshotID); + } } diff --git a/test/token/ERC20/PreserveBalancesOnTransferToken.test.js b/test/token/ERC20/PreserveBalancesOnTransferToken.test.js index 72106b1cee2..5cb495fb723 100644 --- a/test/token/ERC20/PreserveBalancesOnTransferToken.test.js +++ b/test/token/ERC20/PreserveBalancesOnTransferToken.test.js @@ -1,5 +1,8 @@ const BigNumber = web3.BigNumber; + const PreserveBalancesOnTransferToken = artifacts.require('PreserveBalancesOnTransferToken'); +const PreserveBalancesOnTransferTokenMock = artifacts.require('PreserveBalancesOnTransferTokenMock'); +const SnapshotToken = artifacts.require('SnapshotToken'); // Increases ganache time by the passed duration in seconds function increaseTime (duration) { @@ -106,7 +109,6 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { describe('startNewEvent', function () { it('should not allow to create > 20 separate events', async () => { this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.startNewEvent();// 1 await this.token.startNewEvent();// 2 await this.token.startNewEvent();// 3 @@ -256,6 +258,7 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { it('should work correctly if time passed and new event is started', async () => { this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account3, 100); await this.token.mint(account4, 20); @@ -316,6 +319,7 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { // That is a feature, not a bug! it('should work correctly if time NOT passed and new event is started', async () => { this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(account3, 100); await this.token.mint(account4, 20); @@ -395,4 +399,185 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { await this.token.finishEvent(75).should.be.rejectedWith('revert'); }); }); + + describe('createNewSnapshot', function () { + it('should fail due to not owner call', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.createNewSnapshot( + { from: web3.eth.accounts[1] }).should.be.rejectedWith('revert'); + }); + + it('should succeed', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.createNewSnapshot().should.be.fulfilled; + }); + }); + + describe('stopSnapshot', function () { + it('should fail due to not owner call', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + + const tx = await this.token.createNewSnapshot(); + const events = tx.logs.filter(l => l.event === 'SnapshotCreated'); + const snapshotTokenAddress = events[0].args._snapshotTokenAddress; + + this.snapshot = await SnapshotToken.at(snapshotTokenAddress); + await this.token.stopSnapshot( + this.snapshot.address, + { from: web3.eth.accounts[1] } ).should.be.rejectedWith('revert'); + }); + + it('should succeed', async function () { + this.token = await PreserveBalancesOnTransferToken.new(); + + const tx = await this.token.createNewSnapshot(); + const events = tx.logs.filter(l => l.event === 'SnapshotCreated'); + const snapshotTokenAddress = events[0].args._snapshotTokenAddress; + + this.snapshot = await SnapshotToken.at(snapshotTokenAddress); + await this.token.stopSnapshot(this.snapshot.address).should.be.fulfilled; + }); + }); +}); + +contract('SnapshotToken', (accounts) => { + const creator = accounts[0]; + const account3 = accounts[3]; + const account4 = accounts[4]; + const account5 = accounts[5]; + + beforeEach(async function () { + + }); + + describe('start', function () { + it('should fail because not called from the PreserveBalancesOnTransferToken', async function () { + this.token = await PreserveBalancesOnTransferTokenMock.new(); + this.snapshot = await SnapshotToken.new(this.token.address); + + await this.snapshot.start( + { from: web3.eth.accounts[1] }).should.be.rejectedWith('revert'); + }); + + it('should fail if already started', async function () { + this.token = await PreserveBalancesOnTransferTokenMock.new(); + this.snapshot = await SnapshotToken.new(this.token.address); + + await this.token.TEST_callStartForSnapshot(this.snapshot.address).should.be.fulfilled; + await this.token.TEST_callStartForSnapshot(this.snapshot.address).should.be.rejectedWith('revert'); + }); + + it('should succeed', async function () { + this.token = await PreserveBalancesOnTransferTokenMock.new(); + this.snapshot = await SnapshotToken.new(this.token.address); + + await this.token.TEST_callStartForSnapshot(this.snapshot.address).should.be.fulfilled; + }); + }); + + describe('finish', function () { + it('should fail because not called by the owner', async function () { + this.token = await PreserveBalancesOnTransferTokenMock.new(); + + const tx = await this.token.createNewSnapshot(); + const events = tx.logs.filter(l => l.event === 'SnapshotCreated'); + const snapshotTokenAddress = events[0].args._snapshotTokenAddress; + + this.snapshot = await SnapshotToken.at(snapshotTokenAddress); + await this.snapshot.finish( {from: web3.eth.accounts[1]} ).should.be.rejectedWith('revert'); + await this.snapshot.finish().should.be.rejectedWith('revert'); + }); + + it('should fail if was not started', async function () { + this.token = await PreserveBalancesOnTransferTokenMock.new(); + this.snapshot = await SnapshotToken.new(this.token.address); + + await this.token.TEST_callFinishForSnapshot(this.snapshot.address).should.be.rejectedWith('revert'); + }); + + it('should succeed', async function () { + this.token = await PreserveBalancesOnTransferTokenMock.new(); + this.snapshot = await SnapshotToken.new(this.token.address); + + await this.token.TEST_callStartForSnapshot(this.snapshot.address); + await this.token.TEST_callFinishForSnapshot(this.snapshot.address).should.be.fulfilled; + }); + }); + + describe('balanceOf', function () { + it('should fail if not started', async function () { + this.token = await PreserveBalancesOnTransferTokenMock.new(); + await this.token.mint(web3.eth.accounts[0], 1000); + + this.snapshot = await SnapshotToken.new(this.token.address); + let balanceAtStart = await this.snapshot.balanceOf(web3.eth.accounts[0]).should.be.rejectedWith('revert'); + }); + + it('should return correct value if minted some tokens', async function () { + // not a mock contract here! + this.token = await PreserveBalancesOnTransferToken.new(); + + // this calls 'start' automatically + const tx = await this.token.createNewSnapshot(); + const events = tx.logs.filter(l => l.event === 'SnapshotCreated'); + const snapshotTokenAddress = events[0].args._snapshotTokenAddress; + + this.snapshot = await SnapshotToken.at(snapshotTokenAddress); + + await this.token.mint(web3.eth.accounts[0], 1000); + + let balanceReal = await this.token.balanceOf(web3.eth.accounts[0]); + let balanceAtStart = await this.snapshot.balanceOf(web3.eth.accounts[0]); + + assert.equal(balanceReal.toNumber(), 1000); + assert.equal(balanceAtStart.toNumber(), 0); + }); + + it('should return correct value if transferred some tokens', async function () { + // not a mock contract here! + this.token = await PreserveBalancesOnTransferToken.new(); + await this.token.mint(web3.eth.accounts[0], 1000); + + // this calls 'start' automatically + const tx = await this.token.createNewSnapshot(); + const events = tx.logs.filter(l => l.event === 'SnapshotCreated'); + const snapshotTokenAddress = events[0].args._snapshotTokenAddress; + + this.snapshot = await SnapshotToken.at(snapshotTokenAddress); + await this.token.transfer(web3.eth.accounts[1],200); + + let balanceReal = await this.token.balanceOf(web3.eth.accounts[0]); + let balanceReal2 = await this.token.balanceOf(web3.eth.accounts[1]); + let balanceAtStart = await this.snapshot.balanceOf(web3.eth.accounts[0]); + let balanceAtStart2 = await this.snapshot.balanceOf(web3.eth.accounts[1]); + + assert.equal(balanceReal.toNumber(), 800); + assert.equal(balanceReal2.toNumber(), 200); + + assert.equal(balanceAtStart.toNumber(), 1000); + assert.equal(balanceAtStart2.toNumber(), 0); + }); + }); + + describe('transfer', function () { + it('should be blocked', async function () { + this.token = await PreserveBalancesOnTransferTokenMock.new(); + await this.token.mint(web3.eth.accounts[0], 1000); + + this.snapshot = await SnapshotToken.new(this.token.address); + await this.snapshot.transfer(web3.eth.accounts[1],200).should.be.rejectedWith('revert'); + }); + }); + + describe('transferFrom', function () { + it('should be blocked', async function () { + this.token = await PreserveBalancesOnTransferTokenMock.new(); + await this.token.mint(account4, 1000); + + this.snapshot = await SnapshotToken.new(this.token.address); + + await this.snapshot.approve(account3, 1, { from: account4 }); + await this.snapshot.transferFrom(account4, account5, 1, { from: account3 }).should.be.rejectedWith('revert'); + }); + }); }); From 1e266de4cc6b1ae3c320f13abd334b9ec3cf5a86 Mon Sep 17 00:00:00 2001 From: Anthony Akentiev Date: Thu, 2 Aug 2018 15:11:11 +0300 Subject: [PATCH 04/10] Linter: fixes --- .../PreserveBalancesOnTransferTokenMock.sol | 4 +-- .../ERC20/PreserveBalancesOnTransferToken.sol | 27 ++++++++++--------- .../PreserveBalancesOnTransferToken.test.js | 23 ++++++++-------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/contracts/mocks/PreserveBalancesOnTransferTokenMock.sol b/contracts/mocks/PreserveBalancesOnTransferTokenMock.sol index 376b511a228..ab5c0f05f97 100644 --- a/contracts/mocks/PreserveBalancesOnTransferTokenMock.sol +++ b/contracts/mocks/PreserveBalancesOnTransferTokenMock.sol @@ -15,11 +15,11 @@ contract PreserveBalancesOnTransferTokenMock is PreserveBalancesOnTransferToken // do nothing. just for tests } - function TEST_callStartForSnapshot(SnapshotToken _st) public { + function testCallStartForSnapshot(SnapshotToken _st) public { _st.start(); } - function TEST_callFinishForSnapshot(SnapshotToken _st) public { + function testCallFinishForSnapshot(SnapshotToken _st) public { _st.finish(); } } diff --git a/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol b/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol index 856971e4589..266a124b42e 100644 --- a/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol +++ b/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol @@ -226,9 +226,9 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { events[_eventID].eventStartTime); } - function isFromSnapshot(address _a) internal view returns(bool){ - for(uint i = 0; i < snapshotTokens.length; ++i) { - if(snapshotTokens[i] == _a) { + function isFromSnapshot(address _a) internal view returns(bool) { + for (uint i = 0; i < snapshotTokens.length; ++i) { + if (snapshotTokens[i] == _a) { return true; } } @@ -236,6 +236,7 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { } } + /** * @title SnapshotToken * @author Based on code by Thetta DAO Framework: https://github.com/Thetta/Thetta-DAO-Framework/ @@ -248,7 +249,7 @@ contract SnapshotToken is StandardToken, Ownable { bool isStarted = false; constructor(PreserveBalancesOnTransferToken _pbott) public { - pbott = _pbott; + pbott = _pbott; } // BasicToken overrides: @@ -267,8 +268,8 @@ contract SnapshotToken is StandardToken, Ownable { * @param _value The amount to be transferred. */ function transfer(address _to, uint256 _value) public returns (bool) { - revert(); - return false; + revert(); + return false; } // StandardToken overrides: @@ -290,20 +291,20 @@ contract SnapshotToken is StandardToken, Ownable { * @dev Should be called automatically from the PreserveBalancesOnTransferToken */ function start() public { - require(pbott == msg.sender); - require(!isStarted); + require(pbott == msg.sender); + require(!isStarted); - snapshotID = pbott.startNewEvent(); - isStarted = true; + snapshotID = pbott.startNewEvent(); + isStarted = true; } /** * @dev Should be called automatically from the PreserveBalancesOnTransferToken */ function finish() public { - require(pbott == msg.sender); - require(isStarted); + require(pbott == msg.sender); + require(isStarted); - pbott.finishEvent(snapshotID); + pbott.finishEvent(snapshotID); } } diff --git a/test/token/ERC20/PreserveBalancesOnTransferToken.test.js b/test/token/ERC20/PreserveBalancesOnTransferToken.test.js index 5cb495fb723..bf50e3719ac 100644 --- a/test/token/ERC20/PreserveBalancesOnTransferToken.test.js +++ b/test/token/ERC20/PreserveBalancesOnTransferToken.test.js @@ -424,7 +424,7 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { this.snapshot = await SnapshotToken.at(snapshotTokenAddress); await this.token.stopSnapshot( this.snapshot.address, - { from: web3.eth.accounts[1] } ).should.be.rejectedWith('revert'); + { from: web3.eth.accounts[1] }).should.be.rejectedWith('revert'); }); it('should succeed', async function () { @@ -441,7 +441,6 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { }); contract('SnapshotToken', (accounts) => { - const creator = accounts[0]; const account3 = accounts[3]; const account4 = accounts[4]; const account5 = accounts[5]; @@ -463,15 +462,15 @@ contract('SnapshotToken', (accounts) => { this.token = await PreserveBalancesOnTransferTokenMock.new(); this.snapshot = await SnapshotToken.new(this.token.address); - await this.token.TEST_callStartForSnapshot(this.snapshot.address).should.be.fulfilled; - await this.token.TEST_callStartForSnapshot(this.snapshot.address).should.be.rejectedWith('revert'); + await this.token.testCallStartForSnapshot(this.snapshot.address).should.be.fulfilled; + await this.token.testCallStartForSnapshot(this.snapshot.address).should.be.rejectedWith('revert'); }); it('should succeed', async function () { this.token = await PreserveBalancesOnTransferTokenMock.new(); this.snapshot = await SnapshotToken.new(this.token.address); - await this.token.TEST_callStartForSnapshot(this.snapshot.address).should.be.fulfilled; + await this.token.testCallStartForSnapshot(this.snapshot.address).should.be.fulfilled; }); }); @@ -484,7 +483,7 @@ contract('SnapshotToken', (accounts) => { const snapshotTokenAddress = events[0].args._snapshotTokenAddress; this.snapshot = await SnapshotToken.at(snapshotTokenAddress); - await this.snapshot.finish( {from: web3.eth.accounts[1]} ).should.be.rejectedWith('revert'); + await this.snapshot.finish({ from: web3.eth.accounts[1] }).should.be.rejectedWith('revert'); await this.snapshot.finish().should.be.rejectedWith('revert'); }); @@ -492,15 +491,15 @@ contract('SnapshotToken', (accounts) => { this.token = await PreserveBalancesOnTransferTokenMock.new(); this.snapshot = await SnapshotToken.new(this.token.address); - await this.token.TEST_callFinishForSnapshot(this.snapshot.address).should.be.rejectedWith('revert'); + await this.token.testCallFinishForSnapshot(this.snapshot.address).should.be.rejectedWith('revert'); }); it('should succeed', async function () { this.token = await PreserveBalancesOnTransferTokenMock.new(); this.snapshot = await SnapshotToken.new(this.token.address); - await this.token.TEST_callStartForSnapshot(this.snapshot.address); - await this.token.TEST_callFinishForSnapshot(this.snapshot.address).should.be.fulfilled; + await this.token.testCallStartForSnapshot(this.snapshot.address); + await this.token.testCallFinishForSnapshot(this.snapshot.address).should.be.fulfilled; }); }); @@ -510,7 +509,7 @@ contract('SnapshotToken', (accounts) => { await this.token.mint(web3.eth.accounts[0], 1000); this.snapshot = await SnapshotToken.new(this.token.address); - let balanceAtStart = await this.snapshot.balanceOf(web3.eth.accounts[0]).should.be.rejectedWith('revert'); + await this.snapshot.balanceOf(web3.eth.accounts[0]).should.be.rejectedWith('revert'); }); it('should return correct value if minted some tokens', async function () { @@ -544,7 +543,7 @@ contract('SnapshotToken', (accounts) => { const snapshotTokenAddress = events[0].args._snapshotTokenAddress; this.snapshot = await SnapshotToken.at(snapshotTokenAddress); - await this.token.transfer(web3.eth.accounts[1],200); + await this.token.transfer(web3.eth.accounts[1], 200); let balanceReal = await this.token.balanceOf(web3.eth.accounts[0]); let balanceReal2 = await this.token.balanceOf(web3.eth.accounts[1]); @@ -565,7 +564,7 @@ contract('SnapshotToken', (accounts) => { await this.token.mint(web3.eth.accounts[0], 1000); this.snapshot = await SnapshotToken.new(this.token.address); - await this.snapshot.transfer(web3.eth.accounts[1],200).should.be.rejectedWith('revert'); + await this.snapshot.transfer(web3.eth.accounts[1], 200).should.be.rejectedWith('revert'); }); }); From b17dbaaf327cc9e94302b25255d670474e241797 Mon Sep 17 00:00:00 2001 From: Anthony Akentiev Date: Thu, 2 Aug 2018 15:49:30 +0300 Subject: [PATCH 05/10] Fixing linter errors --- .../ERC20/PreserveBalancesOnTransferToken.sol | 4 +- .../PreserveBalancesOnTransferToken.test.js | 70 +++++++++---------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol b/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol index 266a124b42e..7c88834660c 100644 --- a/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol +++ b/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol @@ -101,8 +101,8 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { * @param _amount The amount of tokens to mint. * @return A boolean that indicates if the operation was successful. */ - function mint(address _to, uint256 _amount) canMint onlyOwner - public returns(bool) + function mint(address _to, uint256 _amount) public canMint onlyOwner + returns(bool) { updateCopyOnWriteMap(_to); return super.mint(_to, _amount); diff --git a/test/token/ERC20/PreserveBalancesOnTransferToken.test.js b/test/token/ERC20/PreserveBalancesOnTransferToken.test.js index bf50e3719ac..b0c98cd1481 100644 --- a/test/token/ERC20/PreserveBalancesOnTransferToken.test.js +++ b/test/token/ERC20/PreserveBalancesOnTransferToken.test.js @@ -29,9 +29,9 @@ function increaseTime (duration) { } function increaseTimeTo (target) { - let now = web3.eth.getBlock('latest').timestamp; + const now = web3.eth.getBlock('latest').timestamp; if (target < now) throw Error(`Cannot increase current time(${now}) to a moment in the past(${target})`); - let diff = target - now; + const diff = target - now; return increaseTime(diff); } @@ -54,7 +54,7 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { const account3 = accounts[3]; const account4 = accounts[4]; const account5 = accounts[5]; - + beforeEach(async function () { }); @@ -65,7 +65,7 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { await this.token.mint(web3.eth.accounts[1], 1000, { from: web3.eth.accounts[1] }).should.be.rejectedWith('revert'); }); - + it('should fail with isMintable = false', async function () { this.token = await PreserveBalancesOnTransferToken.new(); await this.token.mint(web3.eth.accounts[1], 1000); @@ -76,11 +76,11 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { await this.token.finishMinting(); await this.token.mint(web3.eth.accounts[1], 1000).should.be.rejectedWith('revert'); }); - + it('should pass', async function () { this.token = await PreserveBalancesOnTransferToken.new(); await this.token.mint(web3.eth.accounts[0], 1000); - let balance = await this.token.balanceOf(web3.eth.accounts[0]); + const balance = await this.token.balanceOf(web3.eth.accounts[0]); assert.equal(balance.toNumber(), 1000); }); }); @@ -91,7 +91,7 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { await this.token.mint(web3.eth.accounts[1], 1000); await this.token.burn(1000, { from: web3.eth.accounts[0] }).should.be.rejectedWith('revert'); }); - + it('should fail due to not enough tokens in the address provided', async function () { this.token = await PreserveBalancesOnTransferToken.new(); await this.token.burn(1000).should.be.rejectedWith('revert'); @@ -101,7 +101,7 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { this.token = await PreserveBalancesOnTransferToken.new(); await this.token.mint(web3.eth.accounts[0], 1000); await this.token.burn(1000); - let balance = await this.token.balanceOf(web3.eth.accounts[0]); + const balance = await this.token.balanceOf(web3.eth.accounts[0]); assert.equal(balance.toNumber(), 0); }); }); @@ -143,8 +143,8 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { this.token = await PreserveBalancesOnTransferToken.new(); await this.token.mint(account4, 1); - let account4Balance = await this.token.balanceOf(account4); - let account5Balance = await this.token.balanceOf(account5); + const account4Balance = await this.token.balanceOf(account4); + const account5Balance = await this.token.balanceOf(account5); assert.equal(account4Balance.toNumber(), 1); assert.equal(account5Balance.toNumber(), 0); @@ -153,8 +153,8 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { const events = tx.logs.filter(l => l.event === 'EventStarted'); const eventID = events.filter(e => e.args._address === creator)[0].args._eventID; - let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); - let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); + const account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + const account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); assert.equal(account4EventBalance.toNumber(), 1); assert.equal(account5EventBalance.toNumber(), 0); @@ -170,11 +170,11 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { await this.token.transfer(account5, 1, { from: account4 }); - let account4Balance = await this.token.balanceOf(account4); - let account5Balance = await this.token.balanceOf(account5); + const account4Balance = await this.token.balanceOf(account4); + const account5Balance = await this.token.balanceOf(account5); - let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); - let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); + const account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + const account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); assert.equal(account4Balance.toNumber(), 0); assert.equal(account5Balance.toNumber(), 1); @@ -192,8 +192,8 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { await this.token.mint(account4, 1); - let account4Balance = await this.token.balanceOf(account4); - let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + const account4Balance = await this.token.balanceOf(account4); + const account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); assert.equal(account4Balance.toNumber(), 1); assert.equal(account4EventBalance.toNumber(), 0); @@ -237,11 +237,11 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { await this.token.approve(account3, 1, { from: account4 }); await this.token.transferFrom(account4, account5, 1, { from: account3 }); - let account4Balance = await this.token.balanceOf(account4); - let account5Balance = await this.token.balanceOf(account5); + const account4Balance = await this.token.balanceOf(account4); + const account5Balance = await this.token.balanceOf(account5); - let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); - let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); + const account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + const account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); assert.equal(account4Balance.toNumber(), 0); assert.equal(account5Balance.toNumber(), 1); @@ -249,7 +249,7 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { assert.equal(account4EventBalance.toNumber(), 1); assert.equal(account5EventBalance.toNumber(), 0); }); - + it('should throw exception because event is not started yet', async function () { this.token = await PreserveBalancesOnTransferToken.new(); await this.token.mint(web3.eth.accounts[0], 1000); @@ -282,7 +282,7 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { // 3 - finish event await this.token.finishEvent(eventID1); // 4 - increase time - let now = web3.eth.getBlock('latest').timestamp; + const now = web3.eth.getBlock('latest').timestamp; await increaseTimeTo(now + duration.seconds(1)); // 5 - create event 2 @@ -335,7 +335,7 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { assert.equal(await this.token.balanceOf(account3), 95); assert.equal(await this.token.balanceOf(account4), 13); assert.equal(await this.token.balanceOf(account5), 12); - + assert.equal(await this.token.getBalanceAtEventStart(eventID1, account3), 100); assert.equal(await this.token.getBalanceAtEventStart(eventID1, account4), 20); assert.equal(await this.token.getBalanceAtEventStart(eventID1, account5), 0); @@ -390,8 +390,8 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { await this.token.transfer(account5, 1, { from: account4 }); - let account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); - let account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); + const account4EventBalance = await this.token.getBalanceAtEventStart(eventID, account4); + const account5EventBalance = await this.token.getBalanceAtEventStart(eventID, account5); assert.equal(account4EventBalance.toNumber(), 1); assert.equal(account5EventBalance.toNumber(), 0); @@ -426,7 +426,7 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { this.snapshot.address, { from: web3.eth.accounts[1] }).should.be.rejectedWith('revert'); }); - + it('should succeed', async function () { this.token = await PreserveBalancesOnTransferToken.new(); @@ -444,7 +444,7 @@ contract('SnapshotToken', (accounts) => { const account3 = accounts[3]; const account4 = accounts[4]; const account5 = accounts[5]; - + beforeEach(async function () { }); @@ -525,8 +525,8 @@ contract('SnapshotToken', (accounts) => { await this.token.mint(web3.eth.accounts[0], 1000); - let balanceReal = await this.token.balanceOf(web3.eth.accounts[0]); - let balanceAtStart = await this.snapshot.balanceOf(web3.eth.accounts[0]); + const balanceReal = await this.token.balanceOf(web3.eth.accounts[0]); + const balanceAtStart = await this.snapshot.balanceOf(web3.eth.accounts[0]); assert.equal(balanceReal.toNumber(), 1000); assert.equal(balanceAtStart.toNumber(), 0); @@ -545,10 +545,10 @@ contract('SnapshotToken', (accounts) => { this.snapshot = await SnapshotToken.at(snapshotTokenAddress); await this.token.transfer(web3.eth.accounts[1], 200); - let balanceReal = await this.token.balanceOf(web3.eth.accounts[0]); - let balanceReal2 = await this.token.balanceOf(web3.eth.accounts[1]); - let balanceAtStart = await this.snapshot.balanceOf(web3.eth.accounts[0]); - let balanceAtStart2 = await this.snapshot.balanceOf(web3.eth.accounts[1]); + const balanceReal = await this.token.balanceOf(web3.eth.accounts[0]); + const balanceReal2 = await this.token.balanceOf(web3.eth.accounts[1]); + const balanceAtStart = await this.snapshot.balanceOf(web3.eth.accounts[0]); + const balanceAtStart2 = await this.snapshot.balanceOf(web3.eth.accounts[1]); assert.equal(balanceReal.toNumber(), 800); assert.equal(balanceReal2.toNumber(), 200); From da493f65c7bbd30627919f9b52946308f0e502a8 Mon Sep 17 00:00:00 2001 From: Anthony Akentiev Date: Thu, 2 Aug 2018 16:12:33 +0300 Subject: [PATCH 06/10] Reverting contracts/mocks/RBACCappedTokenMock.sol --- contracts/mocks/RBACCappedTokenMock.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/mocks/RBACCappedTokenMock.sol b/contracts/mocks/RBACCappedTokenMock.sol index 7147f7b215c..42b6a6bc149 100644 --- a/contracts/mocks/RBACCappedTokenMock.sol +++ b/contracts/mocks/RBACCappedTokenMock.sol @@ -5,10 +5,8 @@ import "../token/ERC20/CappedToken.sol"; contract RBACCappedTokenMock is CappedToken, RBACMintableToken { - constructor( - uint256 _cap - ) + constructor(uint256 _cap) CappedToken(_cap) public {} -} \ No newline at end of file +} From 2ad7df2b501f74d3d82fb067ae8978bc71919321 Mon Sep 17 00:00:00 2001 From: Anthony Akentiev Date: Thu, 2 Aug 2018 16:29:20 +0300 Subject: [PATCH 07/10] Fixing lint warnings --- contracts/mocks/RBACCappedTokenMock.sol | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/mocks/RBACCappedTokenMock.sol b/contracts/mocks/RBACCappedTokenMock.sol index 42b6a6bc149..bf4ff8f2d6a 100644 --- a/contracts/mocks/RBACCappedTokenMock.sol +++ b/contracts/mocks/RBACCappedTokenMock.sol @@ -1,12 +1,12 @@ -pragma solidity ^0.4.24; - -import "../token/ERC20/RBACMintableToken.sol"; -import "../token/ERC20/CappedToken.sol"; - - -contract RBACCappedTokenMock is CappedToken, RBACMintableToken { - constructor(uint256 _cap) - CappedToken(_cap) - public - {} -} +pragma solidity ^0.4.24; + +import "../token/ERC20/RBACMintableToken.sol"; +import "../token/ERC20/CappedToken.sol"; + + +contract RBACCappedTokenMock is CappedToken, RBACMintableToken { + constructor(uint256 _cap) + CappedToken(_cap) + public + {} +} From e31688c61ac43e5bf2e40bcd2529f490e066d929 Mon Sep 17 00:00:00 2001 From: Anthony Akentiev Date: Thu, 2 Aug 2018 16:35:57 +0300 Subject: [PATCH 08/10] Linter fixes --- .../PreserveBalancesOnTransferTokenMock.sol | 2 +- .../PreserveBalancesOnTransferToken.test.js | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/mocks/PreserveBalancesOnTransferTokenMock.sol b/contracts/mocks/PreserveBalancesOnTransferTokenMock.sol index ab5c0f05f97..e485a3a2606 100644 --- a/contracts/mocks/PreserveBalancesOnTransferTokenMock.sol +++ b/contracts/mocks/PreserveBalancesOnTransferTokenMock.sol @@ -3,7 +3,7 @@ pragma solidity ^0.4.24; import "../token/ERC20/PreserveBalancesOnTransferToken.sol"; -contract PreserveBalancesOnTransferTokenMock is PreserveBalancesOnTransferToken { +contract PreserveBalancesMock is PreserveBalancesOnTransferToken { // without modifiers. allow anyone to call this function startNewEvent() public returns(uint) { // do nothing. just for tests diff --git a/test/token/ERC20/PreserveBalancesOnTransferToken.test.js b/test/token/ERC20/PreserveBalancesOnTransferToken.test.js index b0c98cd1481..6fb1543771d 100644 --- a/test/token/ERC20/PreserveBalancesOnTransferToken.test.js +++ b/test/token/ERC20/PreserveBalancesOnTransferToken.test.js @@ -1,7 +1,7 @@ const BigNumber = web3.BigNumber; const PreserveBalancesOnTransferToken = artifacts.require('PreserveBalancesOnTransferToken'); -const PreserveBalancesOnTransferTokenMock = artifacts.require('PreserveBalancesOnTransferTokenMock'); +const PreserveBalancesMock = artifacts.require('PreserveBalancesMock'); const SnapshotToken = artifacts.require('SnapshotToken'); // Increases ganache time by the passed duration in seconds @@ -451,7 +451,7 @@ contract('SnapshotToken', (accounts) => { describe('start', function () { it('should fail because not called from the PreserveBalancesOnTransferToken', async function () { - this.token = await PreserveBalancesOnTransferTokenMock.new(); + this.token = await PreserveBalancesMock.new(); this.snapshot = await SnapshotToken.new(this.token.address); await this.snapshot.start( @@ -459,7 +459,7 @@ contract('SnapshotToken', (accounts) => { }); it('should fail if already started', async function () { - this.token = await PreserveBalancesOnTransferTokenMock.new(); + this.token = await PreserveBalancesMock.new(); this.snapshot = await SnapshotToken.new(this.token.address); await this.token.testCallStartForSnapshot(this.snapshot.address).should.be.fulfilled; @@ -467,7 +467,7 @@ contract('SnapshotToken', (accounts) => { }); it('should succeed', async function () { - this.token = await PreserveBalancesOnTransferTokenMock.new(); + this.token = await PreserveBalancesMock.new(); this.snapshot = await SnapshotToken.new(this.token.address); await this.token.testCallStartForSnapshot(this.snapshot.address).should.be.fulfilled; @@ -476,7 +476,7 @@ contract('SnapshotToken', (accounts) => { describe('finish', function () { it('should fail because not called by the owner', async function () { - this.token = await PreserveBalancesOnTransferTokenMock.new(); + this.token = await PreserveBalancesMock.new(); const tx = await this.token.createNewSnapshot(); const events = tx.logs.filter(l => l.event === 'SnapshotCreated'); @@ -488,14 +488,14 @@ contract('SnapshotToken', (accounts) => { }); it('should fail if was not started', async function () { - this.token = await PreserveBalancesOnTransferTokenMock.new(); + this.token = await PreserveBalancesMock.new(); this.snapshot = await SnapshotToken.new(this.token.address); await this.token.testCallFinishForSnapshot(this.snapshot.address).should.be.rejectedWith('revert'); }); it('should succeed', async function () { - this.token = await PreserveBalancesOnTransferTokenMock.new(); + this.token = await PreserveBalancesMock.new(); this.snapshot = await SnapshotToken.new(this.token.address); await this.token.testCallStartForSnapshot(this.snapshot.address); @@ -505,7 +505,7 @@ contract('SnapshotToken', (accounts) => { describe('balanceOf', function () { it('should fail if not started', async function () { - this.token = await PreserveBalancesOnTransferTokenMock.new(); + this.token = await PreserveBalancesMock.new(); await this.token.mint(web3.eth.accounts[0], 1000); this.snapshot = await SnapshotToken.new(this.token.address); @@ -560,7 +560,7 @@ contract('SnapshotToken', (accounts) => { describe('transfer', function () { it('should be blocked', async function () { - this.token = await PreserveBalancesOnTransferTokenMock.new(); + this.token = await PreserveBalancesMock.new(); await this.token.mint(web3.eth.accounts[0], 1000); this.snapshot = await SnapshotToken.new(this.token.address); @@ -570,7 +570,7 @@ contract('SnapshotToken', (accounts) => { describe('transferFrom', function () { it('should be blocked', async function () { - this.token = await PreserveBalancesOnTransferTokenMock.new(); + this.token = await PreserveBalancesMock.new(); await this.token.mint(account4, 1000); this.snapshot = await SnapshotToken.new(this.token.address); From 1ed04c970b96e19e93a679380a4e2949b378bd52 Mon Sep 17 00:00:00 2001 From: Anthony Akentiev Date: Wed, 8 Aug 2018 17:26:42 +0300 Subject: [PATCH 09/10] Removing unneeded snapshot limit --- .../ERC20/PreserveBalancesOnTransferToken.sol | 22 +++++++++++++--- .../PreserveBalancesOnTransferToken.test.js | 25 +++---------------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol b/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol index 7c88834660c..f7f04a2ee48 100644 --- a/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol +++ b/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol @@ -57,7 +57,7 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { bool isEventInProgress; uint eventStartTime; } - mapping (uint => Event) events; + Event[] events; SnapshotToken[] snapshotTokens; event EventStarted(address indexed _address, uint _eventID); @@ -142,7 +142,8 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { * @return An index of the event started. */ function startNewEvent() public onlyFromSnapshotOrOwner returns(uint) { - for (uint i = 0; i < 20; i++) { + // check if we have empty slots + for (uint i = 0; i < events.length; ++i) { if (!events[i].isEventInProgress) { events[i].isEventInProgress = true; events[i].eventStartTime = now; @@ -151,7 +152,15 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { return i; } } - revert(); //all slots busy at the moment + + // create new event and add to the tail + Event e; + e.isEventInProgress = true; + e.eventStartTime = now; + events.push(e); + + emit EventStarted(msg.sender, events.length - 1); + return (events.length - 1); } /** @@ -159,7 +168,11 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { * @param _eventID An index of the event that was previously returned by startNewEvent(). */ function finishEvent(uint _eventID) public onlyFromSnapshotOrOwner { + require(_eventID < events.length); require(events[_eventID].isEventInProgress); + + // TODO: check that we are from the snapshot + events[_eventID].isEventInProgress = false; emit EventFinished(msg.sender, _eventID); @@ -178,6 +191,7 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { function getBalanceAtEventStart(uint _eventID, address _for) public view returns(uint256) { + require(_eventID < events.length); require(events[_eventID].isEventInProgress); if (!isBalanceWasChangedAfterEventStarted(_eventID, _for)) { @@ -203,7 +217,7 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { } function updateCopyOnWriteMap(address _for) internal { - for (uint i = 0; i < 20; i++) { + for (uint i = 0; i < events.length; ++i) { bool res = isNeedToUpdateBalancesMap(i, _for); if (res) { events[i].holders[_for].balance = balances[_for]; diff --git a/test/token/ERC20/PreserveBalancesOnTransferToken.test.js b/test/token/ERC20/PreserveBalancesOnTransferToken.test.js index 6fb1543771d..6ebbcf089b8 100644 --- a/test/token/ERC20/PreserveBalancesOnTransferToken.test.js +++ b/test/token/ERC20/PreserveBalancesOnTransferToken.test.js @@ -107,29 +107,10 @@ contract('PreserveBalancesOnTransferToken', (accounts) => { }); describe('startNewEvent', function () { - it('should not allow to create > 20 separate events', async () => { + it('should allow to create many separate events', async () => { this.token = await PreserveBalancesOnTransferToken.new(); - await this.token.startNewEvent();// 1 - await this.token.startNewEvent();// 2 - await this.token.startNewEvent();// 3 - await this.token.startNewEvent();// 4 - await this.token.startNewEvent();// 5 - await this.token.startNewEvent();// 6 - await this.token.startNewEvent();// 7 - await this.token.startNewEvent();// 8 - await this.token.startNewEvent();// 9 - await this.token.startNewEvent();// 10 - await this.token.startNewEvent();// 11 - await this.token.startNewEvent();// 12 - await this.token.startNewEvent();// 13 - await this.token.startNewEvent();// 14 - await this.token.startNewEvent();// 15 - await this.token.startNewEvent();// 16 - await this.token.startNewEvent();// 17 - await this.token.startNewEvent();// 18 - await this.token.startNewEvent();// 19 - await this.token.startNewEvent();// 20 - await this.token.startNewEvent().should.be.rejectedWith('revert'); + await this.token.startNewEvent().should.be.fulfilled;// 1 + await this.token.startNewEvent().should.be.fulfilled;// 2 }); it('should not be possible to call by non-owner', async () => { From fe45d95e1a515d21b409a4c713b6120aa4ecd893 Mon Sep 17 00:00:00 2001 From: Anthony Akentiev Date: Wed, 8 Aug 2018 17:31:42 +0300 Subject: [PATCH 10/10] Linter fixes --- .../ERC20/PreserveBalancesOnTransferToken.sol | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol b/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol index f7f04a2ee48..a8344219b2f 100644 --- a/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol +++ b/contracts/token/ERC20/PreserveBalancesOnTransferToken.sol @@ -142,7 +142,7 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { * @return An index of the event started. */ function startNewEvent() public onlyFromSnapshotOrOwner returns(uint) { - // check if we have empty slots + // check if we have empty slots for (uint i = 0; i < events.length; ++i) { if (!events[i].isEventInProgress) { events[i].isEventInProgress = true; @@ -153,14 +153,14 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { } } - // create new event and add to the tail - Event e; - e.isEventInProgress = true; - e.eventStartTime = now; - events.push(e); + // create new event and add to the tail + Event e; + e.isEventInProgress = true; + e.eventStartTime = now; + events.push(e); - emit EventStarted(msg.sender, events.length - 1); - return (events.length - 1); + emit EventStarted(msg.sender, events.length - 1); + return (events.length - 1); } /** @@ -168,10 +168,10 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { * @param _eventID An index of the event that was previously returned by startNewEvent(). */ function finishEvent(uint _eventID) public onlyFromSnapshotOrOwner { - require(_eventID < events.length); + require(_eventID < events.length); require(events[_eventID].isEventInProgress); - // TODO: check that we are from the snapshot + // TODO: check that we are from the snapshot events[_eventID].isEventInProgress = false; @@ -191,7 +191,7 @@ contract PreserveBalancesOnTransferToken is MintableToken, BurnableToken { function getBalanceAtEventStart(uint _eventID, address _for) public view returns(uint256) { - require(_eventID < events.length); + require(_eventID < events.length); require(events[_eventID].isEventInProgress); if (!isBalanceWasChangedAfterEventStarted(_eventID, _for)) {