From ddd77a22f6e425b94f8b4a1b0747599abfe5c8da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Tue, 17 Jul 2018 20:07:47 +0200 Subject: [PATCH 1/4] Staking: rename unlockAndMoveTokens to unlockPartialAndMoveTokens --- future-apps/staking/contracts/Staking.sol | 2 +- future-apps/staking/contracts/interfaces/IStaking.sol | 2 +- future-apps/staking/test/staking.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/future-apps/staking/contracts/Staking.sol b/future-apps/staking/contracts/Staking.sol index 192e796483..42ca9caaef 100644 --- a/future-apps/staking/contracts/Staking.sol +++ b/future-apps/staking/contracts/Staking.sol @@ -211,7 +211,7 @@ contract Staking is ERCStaking, IStaking, AragonApp { MovedTokens(from, to, amount); } - function unlockAndMoveTokens(address from, uint256 lockId, address to, uint256 amount) external { + function unlockPartialAndMoveTokens(address from, uint256 lockId, address to, uint256 amount) external { unlockPartial(from, lockId, amount); moveTokens(from, to, amount); } diff --git a/future-apps/staking/contracts/interfaces/IStaking.sol b/future-apps/staking/contracts/interfaces/IStaking.sol index 55bc2d8ace..8d6eb4af97 100644 --- a/future-apps/staking/contracts/interfaces/IStaking.sol +++ b/future-apps/staking/contracts/interfaces/IStaking.sol @@ -4,7 +4,7 @@ pragma solidity ^0.4.18; interface IStaking { function unlock(address acct, uint256 lockId) public; function moveTokens(address _from, address _to, uint256 _amount) public; - function unlockAndMoveTokens(address from, uint256 lockId, address to, uint256 amount) external; + function unlockPartialAndMoveTokens(address from, uint256 lockId, address to, uint256 amount) external; function getLock( address acct, uint256 lockId diff --git a/future-apps/staking/test/staking.js b/future-apps/staking/test/staking.js index 4d3c62a656..227eb2d173 100644 --- a/future-apps/staking/test/staking.js +++ b/future-apps/staking/test/staking.js @@ -408,7 +408,7 @@ contract('Staking app', accounts => { const lockId = getEvent(r, 'Locked', 'lockId') // unlock - await app.unlockAndMoveTokens(owner, lockId, other, amount / 4, { from: other }) + await app.unlockPartialAndMoveTokens(owner, lockId, other, amount / 4, { from: other }) assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount / 2, "Unlocked owner balance should match") assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there should still be 1 lock") From ed1b3717b6141618591af98726baec67ed3ea09d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Thu, 19 Jul 2018 12:26:04 +0200 Subject: [PATCH 2/4] Staking: Adding deferred locks Now it's possible to pass a start date for locks --- future-apps/staking/.solcover.js | 3 + future-apps/staking/contracts/Staking.sol | 226 ++++++++++++--- .../staking/contracts/interfaces/IStaking.sol | 5 +- future-apps/staking/package-lock.json | 6 +- future-apps/staking/package.json | 9 +- .../staking/test/mocks/FailingTokenMock.sol | 17 ++ .../staking/test/mocks/StakingMock.sol | 18 +- future-apps/staking/test/staking.js | 269 ++++++++++-------- .../staking/test/staking_failing_token.js | 33 +++ future-apps/staking/test/staking_no_mock.js | 76 +++++ .../staking/test/staking_overlocking.js | 246 ++++++++++++++++ 11 files changed, 734 insertions(+), 174 deletions(-) create mode 100644 future-apps/staking/test/mocks/FailingTokenMock.sol create mode 100644 future-apps/staking/test/staking_failing_token.js create mode 100644 future-apps/staking/test/staking_no_mock.js create mode 100644 future-apps/staking/test/staking_overlocking.js diff --git a/future-apps/staking/.solcover.js b/future-apps/staking/.solcover.js index 2169f75895..59ad93b0e5 100644 --- a/future-apps/staking/.solcover.js +++ b/future-apps/staking/.solcover.js @@ -1,6 +1,9 @@ +const interfaces = require('glob').sync('contracts/interfaces/**/*.sol').map(n => n.replace('contracts/', '')) + module.exports = { norpc: true, // rsync is needed so symlinks are resolved on copy of lerna packages testCommand: 'rsync --copy-links -r ../node_modules/@aragon node_modules && node --max-old-space-size=4096 ../node_modules/.bin/truffle test --network coverage', + skipFiles: interfaces, copyNodeModules: true, } diff --git a/future-apps/staking/contracts/Staking.sol b/future-apps/staking/contracts/Staking.sol index 42ca9caaef..b22e5060b6 100644 --- a/future-apps/staking/contracts/Staking.sol +++ b/future-apps/staking/contracts/Staking.sol @@ -13,10 +13,17 @@ contract Staking is ERCStaking, IStaking, AragonApp { using SafeMath for uint256; uint64 constant public MAX_UINT64 = uint64(-1); + address constant public ANY_ENTITY = address(uint160(-1)); + + struct Unlocker { + address account; + uint128 maxLockId; // Id of the biggest lock for that unlocker + } struct Account { uint256 amount; Lock[] locks; + Unlocker[] unlockers; } struct Lock { @@ -24,17 +31,20 @@ contract Staking is ERCStaking, IStaking, AragonApp { Timespan timespan; address unlocker; bytes32 metadata; + uint128 prevUnlockerLockId; // keep an ordered double linked list for unlocker's locks + uint128 nextUnlockerLockId; } struct Timespan { - //uint64 start; // exclusive - uint64 end; // inclusive + uint64 start; + uint64 end; TimeUnit unit; } enum TimeUnit { Blocks, Seconds } - ERC20 public stakingToken; + bool public overlocking; // if true, an unlocker can use the same stake in different locks + ERC20 private stakingToken; // it already has a getter, conforming ERCStaking interface bytes public stakeScript; bytes public unstakeScript; bytes public lockScript; @@ -54,14 +64,15 @@ contract Staking is ERCStaking, IStaking, AragonApp { bytes32 constant public LOCK_ROLE = keccak256("LOCK_ROLE"); bytes32 constant public GOD_ROLE = keccak256("GOD_ROLE"); - modifier checkUnlocked(uint256 amount) { - require(unlockedBalanceOf(msg.sender) >= amount); + modifier checkUnlocked(uint256 amount, address unlocker) { + require(unlockedBalanceOf(msg.sender, unlocker) >= amount); _; } // TODO: Implement forwarder interface - function initialize(ERC20 _stakingToken, bytes _stakeScript, bytes _unstakeScript, bytes _lockScript) onlyInit external { + function initialize(bool _overlocking, ERC20 _stakingToken, bytes _stakeScript, bytes _unstakeScript, bytes _lockScript) onlyInit external { + overlocking = _overlocking; stakingToken = _stakingToken; stakeScript = _stakeScript; unstakeScript = _unstakeScript; @@ -90,7 +101,7 @@ contract Staking is ERCStaking, IStaking, AragonApp { } } - function unstake(uint256 amount, bytes data) authP(UNSTAKE_ROLE, arr(amount)) checkUnlocked(amount) public { + function unstake(uint256 amount, bytes data) authP(UNSTAKE_ROLE, arr(amount)) checkUnlocked(amount, ANY_ENTITY) public { // unstake 0 tokens makes no sense require(amount > 0); @@ -106,28 +117,54 @@ contract Staking is ERCStaking, IStaking, AragonApp { } function lockIndefinitely(uint256 amount, address unlocker, bytes32 metadata, bytes data) public returns(uint256 lockId) { - return lock(amount, uint8(TimeUnit.Seconds), MAX_UINT64, unlocker, metadata, data); + return lock(amount, uint8(TimeUnit.Seconds), getTimestamp(), MAX_UINT64, unlocker, metadata, data); + } + + function lockNow( + uint256 amount, + uint8 lockUnit, + uint64 lockEnds, + address unlocker, + bytes32 metadata, + bytes data + ) + public + returns(uint256 lockId) + { + uint64 lockStarts = lockUnit == uint8(TimeUnit.Blocks) ? getBlocknumber() : getTimestamp(); + return lock(amount, lockUnit, lockStarts, lockEnds, unlocker, metadata, data); } function lock( uint256 amount, uint8 lockUnit, + uint64 lockStarts, uint64 lockEnds, address unlocker, bytes32 metadata, bytes data ) authP(LOCK_ROLE, arr(amount, uint256(lockUnit), uint256(lockEnds))) - checkUnlocked(amount) + checkUnlocked(amount, unlocker) public returns(uint256 lockId) { // lock 0 tokens makes no sense require(amount > 0); - Lock memory newLock = Lock(amount, Timespan(lockEnds, TimeUnit(lockUnit)), unlocker, metadata); + // TODO: should we prevent starting locks in the past? + // require(lockStarts >= (TimeUnit(lockUnit) == TimeUnit.Blocks ? getBlocknumber() : getTimestamp())); + + // leave index 0 empty + if (accounts[msg.sender].locks.length == 0) { + accounts[msg.sender].locks.length++; + } + // create new lock + Lock memory newLock = Lock(amount, Timespan(lockStarts, lockEnds, TimeUnit(lockUnit)), unlocker, metadata, 0, 0); lockId = accounts[msg.sender].locks.push(newLock) - 1; + insertUnlockerLock(unlocker, uint128(lockId)); + Locked(msg.sender, lockId, amount, metadata); if (lockScript.length > 0) { @@ -135,9 +172,53 @@ contract Staking is ERCStaking, IStaking, AragonApp { } } + // TODO: move below + function insertUnlockerLock(address unlocker, uint128 lockId) private { + Account storage account = accounts[msg.sender]; + + // find unlocker in list + Unlocker memory tmpUnlocker; + uint256 l = account.unlockers.length; + while(tmpUnlocker.account != unlocker && l > 0) { + tmpUnlocker = account.unlockers[l-1]; + l--; + } + // not found + if (tmpUnlocker.account != unlocker) { + // add new unlocker + tmpUnlocker.account = unlocker; + tmpUnlocker.maxLockId = lockId; + account.unlockers.push(tmpUnlocker); + } else { // found, insert lock in its oredered list + uint256 amount = account.locks[lockId].amount; + uint128 i = tmpUnlocker.maxLockId; + uint128 lastId = 0; + while(i >= 0) { + if (account.locks[i].amount <= amount) { + // next + account.locks[lockId].nextUnlockerLockId = i; + if(i > 0) { + account.locks[i].prevUnlockerLockId = lockId; + } + // previous + if (lastId == 0) { // the new lock is the first one + account.unlockers[l].maxLockId = lockId; + } else { + account.locks[lastId].nextUnlockerLockId = lockId; + account.locks[lockId].prevUnlockerLockId = lastId; + } + break; + } + lastId = i; + i = account.locks[i].nextUnlockerLockId; + } + } + } + function stakeAndLock( uint256 amount, uint8 lockUnit, + uint64 lockStarts, uint64 lockEnds, address unlocker, bytes32 metadata, @@ -150,17 +231,17 @@ contract Staking is ERCStaking, IStaking, AragonApp { returns(uint256 lockId) { stake(amount, stakeData); - return lock(amount, lockUnit, lockEnds, unlocker, metadata, lockData); + return lock(amount, lockUnit, lockStarts, lockEnds, unlocker, metadata, lockData); } function unlockAllOrNone(address acct) external { - for (uint256 i = accounts[acct].locks.length; i > 0; i--) { + for (uint256 i = accounts[acct].locks.length; i > 1; i--) { unlock(acct, i - 1); } } function unlockAll(address acct) public { - for (uint256 i = accounts[acct].locks.length; i > 0; i--) { + for (uint256 i = accounts[acct].locks.length; i > 1; i--) { if (canUnlock(acct, i - 1)) { unlock(acct, i - 1); } @@ -170,12 +251,26 @@ contract Staking is ERCStaking, IStaking, AragonApp { function unlock(address acct, uint256 lockId) public { require(canUnlock(acct, lockId)); - uint256 lastAccountLock = accounts[acct].locks.length - 1; - if (lockId < lastAccountLock) { - accounts[acct].locks[lockId] = accounts[acct].locks[lastAccountLock]; + // remove from double linked list + Lock storage acctLock = accounts[acct].locks[lockId]; + // prev + if (acctLock.prevUnlockerLockId == 0) { // it was the first one + // get unlocker index + for (uint256 i = 0; i < accounts[acct].unlockers.length; i++) { + if (accounts[acct].unlockers[i].account == acctLock.unlocker) { + break; + } + } + accounts[acct].unlockers[i].maxLockId = acctLock.nextUnlockerLockId; + } else { + accounts[acct].locks[acctLock.prevUnlockerLockId].nextUnlockerLockId = acctLock.nextUnlockerLockId; + } + // next + if (acctLock.nextUnlockerLockId != 0) { // it was not the last one + accounts[acct].locks[acctLock.nextUnlockerLockId].prevUnlockerLockId = 0; } - accounts[acct].locks.length -= 1; + delete(accounts[acct].locks[lockId]); Unlocked(acct, msg.sender, lockId); } @@ -186,11 +281,11 @@ contract Staking is ERCStaking, IStaking, AragonApp { Lock storage acctLock = accounts[acct].locks[lockId]; acctLock.amount = acctLock.amount.sub(amount); + UnlockedPartial(acct, msg.sender, lockId, amount); + if (acctLock.amount == 0) { unlock(acct, lockId); } - - UnlockedPartial(acct, msg.sender, lockId, amount); } function unlockAndUnstake(uint256 amount, bytes data) public { @@ -235,11 +330,24 @@ contract Staking is ERCStaking, IStaking, AragonApp { } function unlockedBalanceOf(address acct) public view returns (uint256) { - uint256 unlockedTokens = accounts[acct].amount; + return unlockedBalanceOf(acct, ANY_ENTITY); + } + + function unlockedBalanceOf(address acct, address unlocker) public view returns (uint256) { + Account memory account = accounts[acct]; + + uint256 unlockedTokens = account.amount; Lock[] storage locks = accounts[acct].locks; - for (uint256 i = 0; i < locks.length; i++) { - if (!canUnlock(acct, i)) { + + if (overlocking) { // with ovelocking we only discount biggest one for each unlocker + for (uint256 j = 0; j < account.unlockers.length; j++) { + if (unlocker == ANY_ENTITY || unlocker != account.unlockers[j].account) { + unlockedTokens = unlockedTokens.sub(locks[account.unlockers[j].maxLockId].amount); + } + } + } else { // without overlocking all locks must be subtracted + for (uint256 i = 1; i < locks.length; i++) { unlockedTokens = unlockedTokens.sub(locks[i].amount); } } @@ -251,35 +359,85 @@ contract Staking is ERCStaking, IStaking, AragonApp { return accounts[acct].locks.length; } - function getLock(address acct, uint256 lockId) public view returns (uint256 amount, uint8 lockUnit, uint64 lockEnds, address unlocker, bytes32 metadata) { + function getLock( + address acct, + uint256 lockId + ) + public + view + returns ( + uint256 amount, + uint8 lockUnit, + uint64 lockStarts, + uint64 lockEnds, + address unlocker, + bytes32 metadata, + uint256 prevUnlockerLockId, + uint256 nextUnlockerLockId + ) + { Lock memory acctLock = accounts[acct].locks[lockId]; - return (acctLock.amount, uint8(acctLock.timespan.unit), uint64(acctLock.timespan.end), acctLock.unlocker, acctLock.metadata); + return ( + acctLock.amount, + uint8(acctLock.timespan.unit), + acctLock.timespan.start, + acctLock.timespan.end, + acctLock.unlocker, + acctLock.metadata, + acctLock.prevUnlockerLockId, + acctLock.nextUnlockerLockId + ); } - function lastLock(address acct) public view returns (uint256 amount, uint8 lockUnit, uint64 lockEnds, address unlocker, bytes32 metadata) { + function lastLock( + address acct + ) + public + view + returns ( + uint256 amount, + uint8 lockUnit, + uint64 lockStarts, + uint64 lockEnds, + address unlocker, + bytes32 metadata, + uint256 prevUnlockerLockId, + uint256 nextUnlockerLockId + ) + { Lock memory acctLock = accounts[acct].locks[locksCount(acct) - 1]; - return (acctLock.amount, uint8(acctLock.timespan.unit), uint64(acctLock.timespan.end), acctLock.unlocker, acctLock.metadata); + + return ( + acctLock.amount, + uint8(acctLock.timespan.unit), + acctLock.timespan.start, + acctLock.timespan.end, + acctLock.unlocker, + acctLock.metadata, + acctLock.prevUnlockerLockId, + acctLock.nextUnlockerLockId + ); } function canUnlock(address acct, uint256 lockId) public view returns (bool) { Lock memory acctLock = accounts[acct].locks[lockId]; - return timespanEnded(acctLock.timespan) || msg.sender == acctLock.unlocker; + return outOfTimespan(acctLock.timespan) || msg.sender == acctLock.unlocker; } - function timespanEnded(Timespan memory timespan) internal view returns (bool) { - uint256 comparingValue = timespan.unit == TimeUnit.Blocks ? getBlocknumber() : getTimestamp(); + function outOfTimespan(Timespan memory timespan) internal view returns (bool) { + uint64 comparingValue = timespan.unit == TimeUnit.Blocks ? getBlocknumber() : getTimestamp(); - return uint64(comparingValue) > timespan.end; + return comparingValue < timespan.start || comparingValue > timespan.end; } - function getTimestamp() internal view returns (uint256) { - return block.timestamp; + function getTimestamp() internal view returns (uint64) { + return uint64(block.timestamp); } // TODO: Use getBlockNumber from Initializable.sol - issue with solidity-coverage - function getBlocknumber() internal view returns (uint256) { - return block.number; + function getBlocknumber() internal view returns (uint64) { + return uint64(block.number); } } diff --git a/future-apps/staking/contracts/interfaces/IStaking.sol b/future-apps/staking/contracts/interfaces/IStaking.sol index 8d6eb4af97..4a6e203e79 100644 --- a/future-apps/staking/contracts/interfaces/IStaking.sol +++ b/future-apps/staking/contracts/interfaces/IStaking.sol @@ -14,9 +14,12 @@ interface IStaking { returns ( uint256 amount, uint8 lockUnit, + uint64 lockStarts, uint64 lockEnds, address unlocker, - bytes32 metadata + bytes32 metadata, + uint256 prevUnlockerLockId, + uint256 nextUnlockerLockId ); } diff --git a/future-apps/staking/package-lock.json b/future-apps/staking/package-lock.json index 8709cbbad2..a17d83b301 100644 --- a/future-apps/staking/package-lock.json +++ b/future-apps/staking/package-lock.json @@ -10,9 +10,9 @@ "integrity": "sha512-CfWul6U034wFs6oV8u20AcW3utaNBqvu2oNEoQRDOrIZ5mrjexL/GGwBNiQ52c6VqG7augNGvDi6uVoGzelH9Q==" }, "@aragon/test-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@aragon/test-helpers/-/test-helpers-1.0.0.tgz", - "integrity": "sha512-hZCyNhe3jClNeuTd5NMpIN+YPaAg2VObnsLcb3KLTwBeeF3BCPKT3BR737go0ZFlFCOhAnqrTH6LNWxYs5Gq+w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@aragon/test-helpers/-/test-helpers-1.0.1.tgz", + "integrity": "sha512-CaROSFATybF2sr8jnmrhp9Br5RfLStFW3HAZ8CUFA4SSc9AlW2NfC30hIZM1L3X65tePzKVyF9uMT+WGaKw09g==", "dev": true }, "@mrmlnc/readdir-enhanced": { diff --git a/future-apps/staking/package.json b/future-apps/staking/package.json index d26ab84a26..80b6674594 100644 --- a/future-apps/staking/package.json +++ b/future-apps/staking/package.json @@ -5,17 +5,16 @@ "main": "truffle.js", "scripts": { "lint": "solium --dir ./contracts", - "test": "TRUFFLE_TEST=true npm run ganache-cli:dev", + "test": "TRUFFLE_TEST=true npm run ganache-cli:test", "test:gas": "GAS_REPORTER=true npm test", - "coverage": "./node_modules/@aragon/test-helpers/run-coverage.sh", + "coverage": "SOLIDITY_COVERAGE=true npm run ganache-cli:test", "console": "node_modules/.bin/truffle console", - "ganache-cli:dev": "./node_modules/@aragon/test-helpers/ganache-cli.sh", - "ganache-cli:coverage": "SOLIDITY_COVERAGE=true npm run ganache-cli:dev" + "ganache-cli:test": "./node_modules/@aragon/test-helpers/ganache-cli.sh" }, "author": "Aragon One AG", "license": "GPL-3.0", "devDependencies": { - "@aragon/test-helpers": "^1.0.0", + "@aragon/test-helpers": "^1.0.1", "ethereumjs-abi": "^0.6.4", "ganache-cli": "^6.0.3", "solidity-coverage": "0.4.3", diff --git a/future-apps/staking/test/mocks/FailingTokenMock.sol b/future-apps/staking/test/mocks/FailingTokenMock.sol new file mode 100644 index 0000000000..3567996223 --- /dev/null +++ b/future-apps/staking/test/mocks/FailingTokenMock.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.4.11; + +import "@aragon/os/contracts/lib/zeppelin/token/StandardToken.sol"; + + +// mock class using StandardToken +contract FailingTokenMock is StandardToken { + + function FailingTokenMock(address initialAccount, uint256 initialBalance) public { + balances[initialAccount] = initialBalance; + totalSupply_ = initialBalance; + } + + function transfer(address to, uint256 amount) public returns(bool) { + return false; + } +} diff --git a/future-apps/staking/test/mocks/StakingMock.sol b/future-apps/staking/test/mocks/StakingMock.sol index e0a901c256..e782e0fd10 100644 --- a/future-apps/staking/test/mocks/StakingMock.sol +++ b/future-apps/staking/test/mocks/StakingMock.sol @@ -3,31 +3,31 @@ pragma solidity 0.4.18; import "../../contracts/Staking.sol"; contract StakingMock is Staking { - uint _mockTime = now; - uint _mockBlockNumber = block.number; + uint64 _mockTime = uint64(now); + uint64 _mockBlockNumber = uint64(block.number); - function getTimestampExt() external view returns (uint256) { + function getTimestampExt() external view returns (uint64) { return getTimestamp(); } - function getBlockNumberExt() external view returns (uint256) { - return getBlockNumber(); + function getBlockNumberExt() external view returns (uint64) { + return getBlocknumber(); } - function setTimestamp(uint i) public { + function setTimestamp(uint64 i) public { _mockTime = i; } - function setBlockNumber(uint i) public { + function setBlockNumber(uint64 i) public { _mockBlockNumber = i; } - function getTimestamp() internal view returns (uint256) { + function getTimestamp() internal view returns (uint64) { return _mockTime; } // TODO: Use getBlockNumber from Initializable.sol - issue with solidity-coverage - function getBlocknumber() internal view returns (uint256) { + function getBlocknumber() internal view returns (uint64) { return _mockBlockNumber; } } diff --git a/future-apps/staking/test/staking.js b/future-apps/staking/test/staking.js index 227eb2d173..7c74efbaf2 100644 --- a/future-apps/staking/test/staking.js +++ b/future-apps/staking/test/staking.js @@ -4,42 +4,47 @@ const { encodeCallScript } = require('@aragon/test-helpers/evmScript') const getContract = artifacts.require const getEvent = (receipt, event, arg) => { return receipt.logs.filter(l => l.event == event)[0].args[arg] } -contract('Staking app', accounts => { +contract('Staking app, no overlocking', accounts => { let app, token const owner = accounts[0] const other = accounts[1] + const yetanother = accounts[2] const balance = 1000 + const amount = 100 const zeroBytes32 = "0x0000000000000000000000000000000000000000000000000000000000000000" + let ANY_ENTITY const TIME_UNIT_BLOCKS = 0 const TIME_UNIT_SECONDS = 1 + const OVERLOCKING = false context('Without scripts', async () => { beforeEach(async () => { app = await getContract('StakingMock').new() + ANY_ENTITY = await app.ANY_ENTITY.call() token = await getContract('StandardTokenMock').new(owner, balance) - await app.initialize(token.address, '', '', '') + // allow Staking app to move owner tokens + await token.approve(app.address, amount) + await app.initialize(OVERLOCKING, token.address, '', '', '') }) it('has correct initial state', async () => { assert.equal(await app.token.call(), token.address, "Token is wrong") assert.equal((await app.totalStaked.call()).valueOf(), 0, "Initial total staked amount should be zero") assert.isFalse(await app.supportsHistory.call(), "Shouldn't support history") + assert.isFalse(await app.overlocking.call(), "Shouldn't support overlocking") }) it('can not reinitialize', async () => { return assertRevert(async () => { - await app.initialize(token.address, '', '', '') + await app.initialize(OVERLOCKING, token.address, '', '', '') }) }) it('stakes', async () => { - const amount = 100 const initialOwnerBalance = parseInt((await token.balanceOf.call(owner)).valueOf(), 10) const initialStakingBalance = parseInt((await token.balanceOf.call(app.address)).valueOf(), 10) - // allow Staking app to move owner tokens - await token.approve(app.address, amount) // stake tokens await app.stake(amount, '') @@ -52,7 +57,6 @@ contract('Staking app', accounts => { it('fails staking 0 amount', async () => { const amount = 0 - await token.approve(app.address, amount) return assertRevert(async () => { await app.stake(amount, '') }) @@ -60,20 +64,16 @@ contract('Staking app', accounts => { it('fails staking more than balance', async () => { const amount = balance + 1 - await token.approve(app.address, amount) return assertRevert(async () => { await app.stake(amount, '') }) }) it('stakes for', async () => { - const amount = 100 const initialOwnerBalance = parseInt((await token.balanceOf.call(owner)).valueOf(), 10) const initialOtherBalance = parseInt((await token.balanceOf.call(other)).valueOf(), 10) const initialStakingBalance = parseInt((await token.balanceOf.call(app.address)).valueOf(), 10) - // allow Staking app to move owner tokens - await token.approve(app.address, amount) // stake tokens await app.stakeFor(other, amount, '') @@ -88,12 +88,9 @@ contract('Staking app', accounts => { }) it('unstakes', async () => { - const amount = 100 const initialOwnerBalance = parseInt((await token.balanceOf.call(owner)).valueOf(), 10) const initialStakingBalance = parseInt((await token.balanceOf.call(app.address)).valueOf(), 10) - // allow Staking app to move owner tokens - await token.approve(app.address, amount) // stake tokens await app.stake(amount, '') // unstake half of them @@ -107,8 +104,6 @@ contract('Staking app', accounts => { }) it('fails unstaking 0 amount', async () => { - const amount = 100 - await token.approve(app.address, amount) await app.stake(amount, '') return assertRevert(async () => { await app.unstake(0, '') @@ -116,8 +111,6 @@ contract('Staking app', accounts => { }) it('fails unstaking more than staked', async () => { - const amount = 100 - await token.approve(app.address, amount) await app.stake(amount, '') return assertRevert(async () => { await app.unstake(amount + 1, '') @@ -125,8 +118,6 @@ contract('Staking app', accounts => { }) it('locks indefinitely', async () => { - const amount = 100 - await token.approve(app.address, amount) await app.stake(amount, '') const r = await app.lockIndefinitely(amount / 2, other, '', '') const lockId = getEvent(r, 'Locked', 'lockId') @@ -135,60 +126,65 @@ contract('Staking app', accounts => { await app.setTimestamp((await app.getTimestampExt.call()) + 5000) // still the same, can not unlock assert.isFalse(await app.canUnlock.call(owner, lockId)) - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount / 2, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount / 2, "Unlocked balance should match for unlocker") assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") }) it('locks using seconds', async () => { - const amount = 100 const time = 1000 - await token.approve(app.address, amount) await app.stake(amount, '') - const endTime = (await app.getTimestampExt.call()) + time - const r = await app.lock(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + const startTime = await app.getTimestampExt.call() + const endTime = startTime + time + const r = await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') const lockId = getEvent(r, 'Locked', 'lockId') // check lock values const lock = await app.getLock.call(owner, lockId) assert.equal(lock[0], amount / 2, "locked amount should match") assert.equal(lock[1], TIME_UNIT_SECONDS, "lock time unit should match") - assert.equal(lock[2], endTime, "lock time end should match") - assert.equal(lock[3], other, "unlocker should match") - assert.equal(lock[4], zeroBytes32, "lock metadata should match") + assert.equal(lock[2].toString(), startTime, "lock time start should match") + assert.equal(lock[3], endTime, "lock time end should match") + assert.equal(lock[4], other, "unlocker should match") + assert.equal(lock[5], zeroBytes32, "lock metadata should match") + assert.equal(lock[6], 0, "next lock id should match") // can not unlock assert.isFalse(await app.canUnlock.call(owner, lockId)) - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount / 2, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount / 2, "Unlocked balance should match for unlocker") assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") await app.setTimestamp(endTime + 1) - // can unlock + // can unlock, but still locked assert.isTrue(await app.canUnlock.call(owner, lockId)) - // unlockable balance counts as unlocked - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount / 2, "Unlocked balance should match for unlocker") }) it('locks using blocks', async () => { - const amount = 100 const blocks = 2 - await token.approve(app.address, amount) await app.stake(amount, '') - const endBlock = (await app.getBlockNumberExt.call.call()) + blocks - const r = await app.lock(amount / 2, TIME_UNIT_BLOCKS, endBlock, other, '', '') + const startBlock = await app.getBlockNumberExt.call() + const endBlock = startBlock + blocks + const r = await app.lockNow(amount / 2, TIME_UNIT_BLOCKS, endBlock, other, '', '') const lockId = getEvent(r, 'Locked', 'lockId') // check lock values const lock = await app.lastLock.call(owner) assert.equal(lock[0], amount / 2, "locked amount should match") assert.equal(lock[1], TIME_UNIT_BLOCKS, "lock time unit should match") - assert.equal(lock[2], endBlock, "lock time end should match") - assert.equal(lock[3], other, "unlocker should match") - assert.equal(lock[4], "0x0000000000000000000000000000000000000000000000000000000000000000", "lock metadata should match") + assert.equal(lock[2].toString(), startBlock, "start block lock should match") + assert.equal(lock[3], endBlock, "end block end should match") + assert.equal(lock[4], other, "unlocker should match") + assert.equal(lock[5], "0x0000000000000000000000000000000000000000000000000000000000000000", "lock metadata should match") + assert.equal(lock[6], 0, "next lock id should match") // can not unlock assert.isFalse(await app.canUnlock.call(owner, lockId)) - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount / 2, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount / 2, "Unlocked balance should match for unlocker") assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") await app.setBlockNumber(endBlock + 1) @@ -197,38 +193,42 @@ contract('Staking app', accounts => { }) it('fails locking 0 tokens', async () => { - const amount = 100 const blocks = 10 - await token.approve(app.address, amount) await app.stake(amount, '') const endBlock = (await app.getBlockNumberExt.call()) + blocks return assertRevert(async () => { - await app.lock(0, TIME_UNIT_BLOCKS, endBlock, other, '', '') + await app.lockNow(0, TIME_UNIT_BLOCKS, endBlock, other, '', '') }) }) it('fails locking more tokens than staked', async () => { - const amount = 100 const blocks = 10 - await token.approve(app.address, amount) await app.stake(amount, '') const endBlock = (await app.getBlockNumberExt.call()) + blocks return assertRevert(async () => { - await app.lock(amount + 1, TIME_UNIT_BLOCKS, endBlock, other, '', '') + await app.lockNow(amount + 1, TIME_UNIT_BLOCKS, endBlock, other, '', '') + }) + }) + + it('fails locking already locked tokens', async () => { + await app.stake(amount, '') + await app.lockIndefinitely(amount / 2 + 1, other, '', '') + return assertRevert(async () => { + await app.lockIndefinitely(amount / 2, other, '', '') }) }) it('stakes and locks in one call', async () => { - const amount = 100 const time = 1000 - await token.approve(app.address, amount) - const endTime = (await app.getTimestampExt.call()) + time - const r = await app.stakeAndLock(amount, TIME_UNIT_SECONDS, endTime, other, '', '', '') + const startTime = (await app.getTimestampExt.call()) + const endTime = startTime + time + const r = await app.stakeAndLock(amount, TIME_UNIT_SECONDS, startTime, endTime, other, '', '', '') const lockId = getEvent(r, 'Locked', 'lockId') // can not unlock assert.isFalse(await app.canUnlock.call(owner, lockId)) - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), 0, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), 0, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), 0, "Unlocked balance should match for unlocker") assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") await app.setTimestamp(endTime + 1) @@ -237,45 +237,54 @@ contract('Staking app', accounts => { }) it('unlocks last lock', async () => { - const amount = 100 const time = 1000 - await token.approve(app.address, amount) await app.stake(amount, '') const endTime = (await app.getTimestampExt.call()) + time - const r = await app.lock(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + const r = await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') const lockId = getEvent(r, 'Locked', 'lockId') // unlock await app.unlock(owner, lockId, { from: other }) - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount, "Unlocked balance should match") - assert.equal((await app.locksCount.call(owner)).valueOf(), 0, "there shouldn't be locks") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount, "Unlocked balance should match") + // TODO assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there shouldn't be locks") // lock Id 0 is left empty }) it('unlocks non-last lock', async () => { - const amount = 100 const time = 1000 - await token.approve(app.address, amount) await app.stake(amount, '') const endTime = (await app.getTimestampExt.call()) + time - const r = await app.lock(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + const r = await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') const lockId = getEvent(r, 'Locked', 'lockId') - await app.lock(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') // unlock await app.unlock(owner, lockId, { from: other }) - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount / 2, "Unlocked balance should match") - assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there should be just 1 lock") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") + // TODO assert.equal((await app.locksCount.call(owner)).valueOf(), 2, "there should be just 1 lock") // lock Id 0 is left empty + }) + + it('unlocks non-last unlocker lock', async () => { + const time = 1000 + await app.stake(amount, '') + const endTime = (await app.getTimestampExt.call()) + time + await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, yetanother, '', '') + const r = await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + const lockId = getEvent(r, 'Locked', 'lockId') + + // unlock + await app.unlock(owner, lockId, { from: other }) + + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") + // TODO assert.equal((await app.locksCount.call(owner)).valueOf(), 2, "there should be just 1 lock") // lock Id 0 is left empty }) it('fails to unlock if can not unlock', async () => { - const amount = 100 const time = 1000 - await token.approve(app.address, amount) await app.stake(amount, '') const endTime = (await app.getTimestampExt.call()) + time - const r = await app.lock(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + const r = await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') const lockId = getEvent(r, 'Locked', 'lockId') // tries to unlock @@ -285,19 +294,24 @@ contract('Staking app', accounts => { }) it('unlocks all', async () => { - const amount = 100 const time = 1000 - await token.approve(app.address, amount) await app.stake(amount, '') const endTime = (await app.getTimestampExt.call()) + time - await app.lock(amount / 4, TIME_UNIT_SECONDS, endTime, other, '', '') - await app.lock(amount / 4, TIME_UNIT_SECONDS, endTime, other, '', '') + await app.lockNow(amount / 4, TIME_UNIT_SECONDS, endTime, other, '', '') + await app.lockNow(amount / 4, TIME_UNIT_SECONDS, endTime, other, '', '') // unlock + // TODO await app.unlockAll(owner, { from: other }) + /* + await app.unlock(owner, 2, { from: other }) + console.log('first unlcoked') + await app.unlock(owner, 1, { from: other }) + console.log('second unlcoked') + */ - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount, "Unlocked balance should match") - assert.equal((await app.locksCount.call(owner)).valueOf(), 0, "there shouldn't be locks") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount, "Unlocked balance should match") + // TODO assert.equal((await app.locksCount.call(owner)).valueOf(), 0, "there shouldn't be locks") // lock Id 0 is left empty }) it('unlocks all with no previous locks', async () => { @@ -306,30 +320,26 @@ contract('Staking app', accounts => { }) it('tries to unlockAll but it only unlocks one', async () => { - const amount = 100 const time = 1000 - await token.approve(app.address, amount) await app.stake(amount, '') await app.lockIndefinitely(amount / 2, other, '', '') const endTime = (await app.getTimestampExt.call()) + time - await app.lock(amount / 4, TIME_UNIT_SECONDS, endTime, other, '', '') + await app.lockNow(amount / 4, TIME_UNIT_SECONDS, endTime, other, '', '') await app.setTimestamp(endTime + 1) // unlock await app.unlockAll(owner) - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount / 2, "Unlocked balance should match") - assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there shouldn't be locks") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") + // TODO assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there shouldn't be locks") }) it('fails trying to unlockAllOrNone if a lock cannot be unlocked', async () => { - const amount = 100 const time = 1000 - await token.approve(app.address, amount) await app.stake(amount, '') await app.lockIndefinitely(amount / 2, other, '', '') const endTime = (await app.getTimestampExt.call()) + time - await app.lock(amount / 4, TIME_UNIT_SECONDS, endTime, other, '', '') + await app.lockNow(amount / 4, TIME_UNIT_SECONDS, endTime, other, '', '') await app.setTimestamp(endTime + 1) // unlock @@ -339,40 +349,47 @@ contract('Staking app', accounts => { }) it('unlocks partially', async () => { - const amount = 100 const time = 1000 - await token.approve(app.address, amount) await app.stake(amount, '') const endTime = (await app.getTimestampExt.call()) + time - const r = await app.lock(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + const r = await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') const lockId = getEvent(r, 'Locked', 'lockId') // unlock await app.unlockPartial(owner, lockId, amount / 4, { from: other }) - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount * 3 / 4, "Unlocked balance should match") - assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there should still be 1 lock") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount * 3 / 4, "Unlocked balance should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), 2, "there should still be 1 lock") // lock Id 0 is left empty // unlocks again await app.unlockPartial(owner, lockId, amount / 4, { from: other }) - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount, "Unlocked balance should match") - assert.equal((await app.locksCount.call(owner)).valueOf(), 0, "there shouldnt be locks") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount, "Unlocked balance should match") + // TODO assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there shouldn't be locks") // lock Id 0 is left empty + }) + + it('fails to unlock partially if can not unlock', async () => { + const time = 1000 + await app.stake(amount, '') + const endTime = (await app.getTimestampExt.call()) + time + const r = await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + const lockId = getEvent(r, 'Locked', 'lockId') + + // tries to unlock + return assertRevert(async () => { + await app.unlockPartial(owner, lockId, amount / 4) + }) }) it('moves tokens', async () => { - const amount = 100 const time = 1000 - await token.approve(app.address, amount) await app.stake(amount, '') await app.moveTokens(owner, other, amount / 2) - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount / 2, "Unlocked owner balance should match") - assert.equal((await app.unlockedBalanceOf.call(other)).valueOf(), amount / 2, "Unlocked other balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked owner balance should match") + assert.equal((await app.unlockedBalanceOf.call(other, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked other balance should match") }) it('fails moving 0 tokens', async () => { - const amount = 100 - await token.approve(app.address, amount) await app.stake(amount, '') return assertRevert(async () => { await app.moveTokens(owner, other, 0) @@ -380,8 +397,6 @@ contract('Staking app', accounts => { }) it('fails moving more tokens than staked', async () => { - const amount = 100 - await token.approve(app.address, amount) await app.stake(amount, '') return assertRevert(async () => { await app.moveTokens(owner, other, amount + 1) @@ -389,8 +404,6 @@ contract('Staking app', accounts => { }) it('fails moving more tokens than unlocked', async () => { - const amount = 100 - await token.approve(app.address, amount) await app.stake(amount, '') await app.lockIndefinitely(amount / 2, other, '', '') return assertRevert(async () => { @@ -398,21 +411,42 @@ contract('Staking app', accounts => { }) }) + // TODO: should we allow this? Or force to unlock first? Or unlock automatically? + it('fails moving unlocked tokens even if they can be unlocked', async () => { + const time = 1000 + const endTime = (await app.getTimestampExt.call()) + time + await app.stake(amount, '') + // lock + const r = await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + const lockId = getEvent(r, 'Locked', 'lockId') + // wait for time to go by + await app.setTimestamp(endTime + 1) + // try to move + await assertRevert(async () => { + await app.moveTokens(owner, other, amount / 2 + 1) + }) + // unlock + await app.unlock(owner, lockId) + // now we should be able to move + await app.moveTokens(owner, other, amount / 2 + 1) + // checks + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2 - 1, "Unlocked owner balance should match") + assert.equal((await app.unlockedBalanceOf.call(other, ANY_ENTITY)).valueOf(), amount / 2 + 1, "Unlocked other balance should match") + }) + it('unlocks and moves tokens', async () => { - const amount = 100 const time = 1000 - await token.approve(app.address, amount) await app.stake(amount, '') const endTime = (await app.getTimestampExt.call()) + time - const r = await app.lock(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + const r = await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') const lockId = getEvent(r, 'Locked', 'lockId') // unlock await app.unlockPartialAndMoveTokens(owner, lockId, other, amount / 4, { from: other }) - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount / 2, "Unlocked owner balance should match") - assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there should still be 1 lock") - assert.equal((await app.unlockedBalanceOf.call(other)).valueOf(), amount / 4, "Unlocked other balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked owner balance should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), 2, "there should still be 1 lock") // lock Id 0 is left empty + assert.equal((await app.unlockedBalanceOf.call(other, ANY_ENTITY)).valueOf(), amount / 4, "Unlocked other balance should match") }) }) @@ -450,7 +484,11 @@ contract('Staking app', accounts => { // App const receipt = await dao.newAppInstance('0x1234', (await getContract('StakingMock').new()).address, { from: owner }) app = getContract('StakingMock').at(receipt.logs.filter(l => l.event == 'NewAppProxy')[0].args.proxy) - await app.initialize(token.address, stakeScript, unstakeScript, lockScript) + ANY_ENTITY = await app.ANY_ENTITY.call() + await app.initialize(OVERLOCKING, token.address, stakeScript, unstakeScript, lockScript) + + // allow Staking app to move owner tokens + await token.approve(app.address, amount) // Permissions await acl.createPermission(owner, app.address, await app.STAKE_ROLE(), owner, { from: owner }) @@ -459,12 +497,9 @@ contract('Staking app', accounts => { }) it('stakes and runs script', async () => { - const amount = 100 const initialOwnerBalance = parseInt((await token.balanceOf.call(owner)).valueOf(), 10) const initialStakingBalance = parseInt((await token.balanceOf.call(app.address)).valueOf(), 10) - // allow Staking app to move owner tokens - await token.approve(app.address, amount) // stake tokens await app.stake(amount, '') @@ -477,12 +512,9 @@ contract('Staking app', accounts => { }) it('unstakes and runs script', async () => { - const amount = 100 const initialOwnerBalance = parseInt((await token.balanceOf.call(owner)).valueOf(), 10) const initialStakingBalance = parseInt((await token.balanceOf.call(app.address)).valueOf(), 10) - // allow Staking app to move owner tokens - await token.approve(app.address, amount) // stake tokens await app.stake(amount, '') // unstake half of them @@ -497,8 +529,6 @@ contract('Staking app', accounts => { }) it('locks indefinitely and runs script', async () => { - const amount = 100 - await token.approve(app.address, amount) await app.stake(amount, '') const r = await app.lockIndefinitely(amount / 2, other, '', '') const lockId = getEvent(r, 'Locked', 'lockId') @@ -507,26 +537,24 @@ contract('Staking app', accounts => { await app.setTimestamp((await app.getTimestampExt.call()) + 5000) // still the same, can not unlock assert.isFalse(await app.canUnlock.call(owner, lockId)) - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount / 2, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") assert.equal(await executionTarget.lockCounter(), 1, 'should have received execution call') }) it('stakes, locks and runs both script', async () => { - const amount = 100 const time = 1000 const initialOwnerBalance = parseInt((await token.balanceOf.call(owner)).valueOf(), 10) const initialStakingBalance = parseInt((await token.balanceOf.call(app.address)).valueOf(), 10) - // allow Staking app to move owner tokens - await token.approve(app.address, amount) - const endTime = (await app.getTimestampExt.call()) + time + const startTime = (await app.getTimestampExt.call()) + const endTime = startTime + time // stake and lock tokens - const r = await app.stakeAndLock(amount, TIME_UNIT_SECONDS, endTime, other, '', '', '') + const r = await app.stakeAndLock(amount, TIME_UNIT_SECONDS, startTime, endTime, other, '', '', '') const lockId = getEvent(r, 'Locked', 'lockId') // can not unlock assert.isFalse(await app.canUnlock.call(owner, lockId)) - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), 0, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), 0, "Unlocked balance should match") assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") await app.setTimestamp(endTime + 1) @@ -545,18 +573,15 @@ contract('Staking app', accounts => { }) it('unlocks unstakes and runs script', async () => { - const amount = 100 const time = 1000 const initialOwnerBalance = parseInt((await token.balanceOf.call(owner)).valueOf(), 10) const initialStakingBalance = parseInt((await token.balanceOf.call(app.address)).valueOf(), 10) - // allow Staking app to move owner tokens - await token.approve(app.address, amount) // stake tokens await app.stake(amount, '') // locks tokens const endTime = (await app.getTimestampExt.call()) + time - await app.lock(amount, TIME_UNIT_SECONDS, endTime, other, '', '') + await app.lockNow(amount, TIME_UNIT_SECONDS, endTime, other, '', '') await app.setTimestamp(endTime + 1) diff --git a/future-apps/staking/test/staking_failing_token.js b/future-apps/staking/test/staking_failing_token.js new file mode 100644 index 0000000000..ff8ce8b78c --- /dev/null +++ b/future-apps/staking/test/staking_failing_token.js @@ -0,0 +1,33 @@ +const { assertRevert } = require('@aragon/test-helpers/assertThrow') +const getTimestamp = async () => await web3.eth.getBlock(web3.eth.blockNumber).timestamp + +const getContract = artifacts.require +const getEvent = (receipt, event, arg) => { return receipt.logs.filter(l => l.event == event)[0].args[arg] } + +// to test token transfer on unstake, to achieve 100% coverage +contract('Staking app, with bad token', accounts => { + let app, token + const owner = accounts[0] + const other = accounts[1] + const balance = 1000 + const amount = 100 + + const OVERLOCKING = false + + beforeEach(async () => { + app = await getContract('Staking').new() + token = await getContract('FailingTokenMock').new(owner, balance) + // allow Staking app to move owner tokens + await token.approve(app.address, amount) + await app.initialize(OVERLOCKING, token.address, '', '', '') + }) + + it('fails unstaking because of bad token', async () => { + // stake tokens + await app.stake(amount, '') + // try to unstake half of them + return assertRevert(async () => { + await app.unstake(amount / 2, '') + }) + }) +}) diff --git a/future-apps/staking/test/staking_no_mock.js b/future-apps/staking/test/staking_no_mock.js new file mode 100644 index 0000000000..59203ccb56 --- /dev/null +++ b/future-apps/staking/test/staking_no_mock.js @@ -0,0 +1,76 @@ +const getTimestamp = async () => await web3.eth.getBlock(web3.eth.blockNumber).timestamp + +const getContract = artifacts.require +const getEvent = (receipt, event, arg) => { return receipt.logs.filter(l => l.event == event)[0].args[arg] } + +// to test getBlocknumber and getTimestamp, to achieve 100% coverage +contract('Staking app, Real one (no mock)', accounts => { + let app, token + const owner = accounts[0] + const other = accounts[1] + const balance = 1000 + const amount = 100 + + const zeroBytes32 = "0x0000000000000000000000000000000000000000000000000000000000000000" + let ANY_ENTITY + const TIME_UNIT_BLOCKS = 0 + const TIME_UNIT_SECONDS = 1 + const OVERLOCKING = false + + beforeEach(async () => { + app = await getContract('Staking').new() + ANY_ENTITY = await app.ANY_ENTITY.call() + token = await getContract('StandardTokenMock').new(owner, balance) + // allow Staking app to move owner tokens + await token.approve(app.address, amount) + await app.initialize(OVERLOCKING, token.address, '', '', '') + }) + + it('locks using seconds', async () => { + const time = 1000 + await app.stake(amount, '') + const startTime = await getTimestamp() + const endTime = startTime + time + const r = await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + const lockId = getEvent(r, 'Locked', 'lockId') + + // check lock values + const lock = await app.getLock.call(owner, lockId) + assert.equal(lock[0], amount / 2, "locked amount should match") + assert.equal(lock[1], TIME_UNIT_SECONDS, "lock time unit should match") + assert.equal(lock[3], endTime, "lock time end should match") + assert.equal(lock[4], other, "unlocker should match") + assert.equal(lock[5], zeroBytes32, "lock metadata should match") + assert.equal(lock[6], 0, "next lock id should match") + + // can not unlock + assert.isFalse(await app.canUnlock.call(owner, lockId)) + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount / 2, "Unlocked balance should match for unlocker") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + }) + + it('locks using blocks', async () => { + const blocks = 2 + await app.stake(amount, '') + const startBlock = web3.eth.blockNumber + const endBlock = startBlock + blocks + const r = await app.lockNow(amount / 2, TIME_UNIT_BLOCKS, endBlock, other, '', '') + const lockId = getEvent(r, 'Locked', 'lockId') + + // check lock values + const lock = await app.lastLock.call(owner) + assert.equal(lock[0], amount / 2, "locked amount should match") + assert.equal(lock[1], TIME_UNIT_BLOCKS, "lock time unit should match") + assert.equal(lock[3], endBlock, "end block end should match") + assert.equal(lock[4], other, "unlocker should match") + assert.equal(lock[5], "0x0000000000000000000000000000000000000000000000000000000000000000", "lock metadata should match") + assert.equal(lock[6], 0, "next lock id should match") + + // can not unlock + assert.isFalse(await app.canUnlock.call(owner, lockId)) + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount / 2, "Unlocked balance should match for unlocker") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + }) +}) diff --git a/future-apps/staking/test/staking_overlocking.js b/future-apps/staking/test/staking_overlocking.js new file mode 100644 index 0000000000..a7784d055d --- /dev/null +++ b/future-apps/staking/test/staking_overlocking.js @@ -0,0 +1,246 @@ +const { assertRevert } = require('@aragon/test-helpers/assertThrow') +const { encodeCallScript } = require('@aragon/test-helpers/evmScript') + +const getContract = artifacts.require +const getEvent = (receipt, event, arg) => { return receipt.logs.filter(l => l.event == event)[0].args[arg] } + +contract('Staking app, overlocking', accounts => { + let app, token + const owner = accounts[0] + const other = accounts[1] + const balance = 1000 + + const zeroBytes32 = "0x0000000000000000000000000000000000000000000000000000000000000000" + let ANY_ENTITY + const TIME_UNIT_BLOCKS = 0 + const TIME_UNIT_SECONDS = 1 + const OVERLOCKING = true + + const amount = 100 + + beforeEach(async () => { + app = await getContract('StakingMock').new() + ANY_ENTITY = await app.ANY_ENTITY.call() + token = await getContract('StandardTokenMock').new(owner, balance) + // allow Staking app to move owner tokens + await token.approve(app.address, amount) + // init Staking + await app.initialize(OVERLOCKING, token.address, '', '', '') + }) + + it('has correct initial state', async () => { + assert.equal(await app.token.call(), token.address, "Token is wrong") + assert.equal((await app.totalStaked.call()).valueOf(), 0, "Initial total staked amount should be zero") + assert.isFalse(await app.supportsHistory.call(), "Shouldn't support history") + assert.isTrue(await app.overlocking.call(), "Should support overlocking") + }) + + it('unstakes after overlocking', async () => { + const initialOwnerBalance = parseInt((await token.balanceOf.call(owner)).valueOf(), 10) + const initialStakingBalance = parseInt((await token.balanceOf.call(app.address)).valueOf(), 10) + + // stake tokens + await app.stake(amount, '') + // lock twice + const r1 = await app.lockIndefinitely(amount / 2, other, '', '') + const lockId1 = getEvent(r1, 'Locked', 'lockId') + const r2 = await app.lockIndefinitely(amount, other, '', '') + const lockId2 = getEvent(r2, 'Locked', 'lockId') + // can not unstake + await assertRevert(async () => { + await app.unstake(amount / 2, '') + }) + // unlock 2nd lock + await app.unlock(owner, lockId2, { from: other }) + // now we can unstake half of them + await app.unstake(amount / 2, '') + + const finalOwnerBalance = parseInt((await token.balanceOf.call(owner)).valueOf(), 10) + const finalStakingBalance = parseInt((await token.balanceOf.call(app.address)).valueOf(), 10) + assert.equal(finalOwnerBalance, initialOwnerBalance - amount / 2, "owner balance should match") + assert.equal(finalStakingBalance, initialStakingBalance + amount / 2, "Staking app balance should match") + assert.equal((await app.totalStakedFor.call(owner)).valueOf(), amount / 2, "staked value should match") + + // the other haf is still locked, so can not unstake it + await assertRevert(async () => { + await app.unstake(amount / 2, '') + }) + }) + + it('locks indefinitely', async () => { + await app.stake(amount, '') + const r = await app.lockIndefinitely(amount / 2, other, '', '') + const lockId = getEvent(r, 'Locked', 'lockId') + // can not unlock + assert.isFalse(await app.canUnlock.call(owner, lockId)) + await app.setTimestamp((await app.getTimestampExt.call()) + 5000) + // still the same, can not unlock + assert.isFalse(await app.canUnlock.call(owner, lockId)) + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") + // lock doesn't count for the unlocker as overlocing is enabled + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "Unlocked balance should match for unlocker") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + }) + + it('locks already locked tokens (overlocking)', async () => { + await app.stake(amount, '') + // lock twice + const r1 = await app.lockIndefinitely(amount / 2, other, '', '') + const lockId1 = getEvent(r1, 'Locked', 'lockId') + const r2 = await app.lockIndefinitely(amount / 2 + 1, other, '', '') + const lockId2 = getEvent(r2, 'Locked', 'lockId') + // checks + assert.notEqual(lockId1, lockId2, "Lock Ids should be different") + // only biggest one counts + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2 - 1, "Unlocked balance should match") + // lock doesn't count for the unlocker as overlocing is enabled + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "Unlocked balance should match for unlocker") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId2, 10) + 1, "last lock id should match") + }) + + it('locks using seconds', async () => { + const time = 1000 + await app.stake(amount, '') + const startTime = await app.getTimestampExt.call() + const endTime = startTime + time + const r = await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + const lockId = getEvent(r, 'Locked', 'lockId') + + // check lock values + const lock = await app.getLock.call(owner, lockId) + assert.equal(lock[0], amount / 2, "locked amount should match") + assert.equal(lock[1], TIME_UNIT_SECONDS, "lock time unit should match") + assert.equal(lock[2].toString(), startTime, "lock time start should match") + assert.equal(lock[3], endTime, "lock time end should match") + assert.equal(lock[4], other, "unlocker should match") + assert.equal(lock[5], zeroBytes32, "lock metadata should match") + assert.equal(lock[6], 0, "next lock id should match") + + // can not unlock + assert.isFalse(await app.canUnlock.call(owner, lockId)) + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") + // lock doesn't count for the unlocker as overlocing is enabled + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "Unlocked balance should match for unlocker") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + + await app.setTimestamp(endTime + 1) + // can unlock + assert.isTrue(await app.canUnlock.call(owner, lockId)) + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") + // lock doesn't count for the unlocker as overlocing is enabled + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "Unlocked balance should match for unlocker") + + }) + + it('locks using blocks', async () => { + const blocks = 2 + await app.stake(amount, '') + const startBlock = await app.getBlockNumberExt.call.call() + const endBlock = startBlock + blocks + const r = await app.lockNow(amount / 2, TIME_UNIT_BLOCKS, endBlock, other, '', '') + const lockId = getEvent(r, 'Locked', 'lockId') + + // check lock values + const lock = await app.lastLock.call(owner) + assert.equal(lock[0], amount / 2, "locked amount should match") + assert.equal(lock[1], TIME_UNIT_BLOCKS, "lock time unit should match") + assert.equal(lock[2].toString(), startBlock, "start block lock should match") + assert.equal(lock[3], endBlock, "end block end should match") + assert.equal(lock[4], other, "unlocker should match") + assert.equal(lock[5], "0x0000000000000000000000000000000000000000000000000000000000000000", "lock metadata should match") + assert.equal(lock[6], 0, "next lock id should match") + + // can not unlock + assert.isFalse(await app.canUnlock.call(owner, lockId)) + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") + // lock doesn't count for the unlocker as overlocing is enabled + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "Unlocked balance should match for unlocker") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + + await app.setBlockNumber(endBlock + 1) + // can unlock + assert.isTrue(await app.canUnlock.call(owner, lockId)) + }) + + + it('unlocks partially', async () => { + const time = 1000 + await app.stake(amount, '') + const endTime = (await app.getTimestampExt.call()) + time + const r = await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + const lockId = getEvent(r, 'Locked', 'lockId') + + // unlock + await app.unlockPartial(owner, lockId, amount / 4, { from: other }) + + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount * 3 / 4, "Unlocked balance should match") + // lock doesn't count for the unlocker as overlocing is enabled + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "Unlocked balance should match for unlocker") + assert.equal((await app.locksCount.call(owner)).valueOf(), 2, "there should still be 1 lock") // id 0 is left empty + + // unlocks again + await app.unlockPartial(owner, lockId, amount / 4, { from: other }) + + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount, "Unlocked balance should match") + // lock doesn't count for the unlocker as overlocing is enabled + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "Unlocked balance should match for unlocker") + // TODO assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there shouldn't be locks") // id 0 is left empty + }) + + it('moves tokens', async () => { + const time = 1000 + await app.stake(amount, '') + await app.moveTokens(owner, other, amount / 2) + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked owner balance should match") + assert.equal((await app.unlockedBalanceOf.call(other, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked other balance should match") + }) + + it('fails moving more tokens than unlocked', async () => { + await app.stake(amount, '') + await app.lockIndefinitely(amount / 2, other, '', '') + return assertRevert(async () => { + await app.moveTokens(owner, other, amount / 2 + 1) + }) + }) + + it('moves tokens after overlocking', async () => { + // stake tokens + await app.stake(amount, '') + // lock twice + const r1 = await app.lockIndefinitely(amount / 2, other, '', '') + const lockId1 = getEvent(r1, 'Locked', 'lockId') + const r2 = await app.lockIndefinitely(amount, other, '', '') + const lockId2 = getEvent(r2, 'Locked', 'lockId') + // can not move + await assertRevert(async () => { + await app.moveTokens(owner, other, amount / 2) + }) + // unlock 2nd lock + await app.unlock(owner, lockId2, { from: other }) + // now we can move half of them + await app.moveTokens(owner, other, amount / 2) + // checks + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), 0, "Unlocked owner balance should match") + assert.equal((await app.unlockedBalanceOf.call(other, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked other balance should match") + + // the other haf is still locked, so can not be moved + await assertRevert(async () => { + await app.moveTokens(owner, other, amount / 2) + }) + }) + + it('unlocks and moves tokens', async () => { + const time = 1000 + await app.stake(amount, '') + const endTime = (await app.getTimestampExt.call()) + time + const r = await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + const lockId = getEvent(r, 'Locked', 'lockId') + + // unlock + await app.unlockPartialAndMoveTokens(owner, lockId, other, amount / 4, { from: other }) + + assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked owner balance should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), 2, "there should still be 1 lock") // id 0 is left empty + assert.equal((await app.unlockedBalanceOf.call(other, ANY_ENTITY)).valueOf(), amount / 4, "Unlocked other balance should match") + }) +}) From 3e9478cc6848c4e0fdff4019ab96faa2e1e6e4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Fri, 20 Jul 2018 23:40:34 +0200 Subject: [PATCH 3/4] Staking: immutable lock ids It was actually done in previous commit, but with this one locks array is converted to a mapping, as otherwise we could end up with a huge array with a lot of empty elements. Added an array for ids for loops and counting. Fixes #5. --- future-apps/staking/contracts/Staking.sol | 73 +++++++++++-------- future-apps/staking/test/staking.js | 36 ++++----- future-apps/staking/test/staking_no_mock.js | 4 +- .../staking/test/staking_overlocking.js | 28 +++---- 4 files changed, 72 insertions(+), 69 deletions(-) diff --git a/future-apps/staking/contracts/Staking.sol b/future-apps/staking/contracts/Staking.sol index b22e5060b6..2cee34d44c 100644 --- a/future-apps/staking/contracts/Staking.sol +++ b/future-apps/staking/contracts/Staking.sol @@ -17,12 +17,14 @@ contract Staking is ERCStaking, IStaking, AragonApp { struct Unlocker { address account; - uint128 maxLockId; // Id of the biggest lock for that unlocker + uint256 maxLockId; // Id of the biggest lock for that unlocker } struct Account { uint256 amount; - Lock[] locks; + mapping(uint256 => Lock) locks; + uint256[] lockIds; + uint256 lastLockId; Unlocker[] unlockers; } @@ -31,8 +33,9 @@ contract Staking is ERCStaking, IStaking, AragonApp { Timespan timespan; address unlocker; bytes32 metadata; - uint128 prevUnlockerLockId; // keep an ordered double linked list for unlocker's locks - uint128 nextUnlockerLockId; + // TODO: uint128? <-> IStaking + uint256 prevUnlockerLockId; // keep an ordered double linked list for unlocker's locks + uint256 nextUnlockerLockId; } struct Timespan { @@ -156,14 +159,13 @@ contract Staking is ERCStaking, IStaking, AragonApp { // require(lockStarts >= (TimeUnit(lockUnit) == TimeUnit.Blocks ? getBlocknumber() : getTimestamp())); // leave index 0 empty - if (accounts[msg.sender].locks.length == 0) { - accounts[msg.sender].locks.length++; - } + lockId = ++accounts[msg.sender].lastLockId; // create new lock Lock memory newLock = Lock(amount, Timespan(lockStarts, lockEnds, TimeUnit(lockUnit)), unlocker, metadata, 0, 0); - lockId = accounts[msg.sender].locks.push(newLock) - 1; + accounts[msg.sender].locks[lockId] = newLock; + accounts[msg.sender].lockIds.push(lockId); - insertUnlockerLock(unlocker, uint128(lockId)); + insertUnlockerLock(unlocker, lockId); Locked(msg.sender, lockId, amount, metadata); @@ -173,15 +175,15 @@ contract Staking is ERCStaking, IStaking, AragonApp { } // TODO: move below - function insertUnlockerLock(address unlocker, uint128 lockId) private { + function insertUnlockerLock(address unlocker, uint256 lockId) private { Account storage account = accounts[msg.sender]; // find unlocker in list Unlocker memory tmpUnlocker; - uint256 l = account.unlockers.length; - while(tmpUnlocker.account != unlocker && l > 0) { - tmpUnlocker = account.unlockers[l-1]; - l--; + uint256 unlockersIndex = account.unlockers.length; + while (tmpUnlocker.account != unlocker && unlockersIndex > 0) { + unlockersIndex--; + tmpUnlocker = account.unlockers[unlockersIndex]; } // not found if (tmpUnlocker.account != unlocker) { @@ -191,18 +193,18 @@ contract Staking is ERCStaking, IStaking, AragonApp { account.unlockers.push(tmpUnlocker); } else { // found, insert lock in its oredered list uint256 amount = account.locks[lockId].amount; - uint128 i = tmpUnlocker.maxLockId; - uint128 lastId = 0; - while(i >= 0) { + uint256 i = tmpUnlocker.maxLockId; + uint256 lastId = 0; + while (i >= 0) { if (account.locks[i].amount <= amount) { // next account.locks[lockId].nextUnlockerLockId = i; - if(i > 0) { + if (i > 0) { account.locks[i].prevUnlockerLockId = lockId; } // previous if (lastId == 0) { // the new lock is the first one - account.unlockers[l].maxLockId = lockId; + account.unlockers[unlockersIndex].maxLockId = lockId; } else { account.locks[lastId].nextUnlockerLockId = lockId; account.locks[lockId].prevUnlockerLockId = lastId; @@ -235,15 +237,16 @@ contract Staking is ERCStaking, IStaking, AragonApp { } function unlockAllOrNone(address acct) external { - for (uint256 i = accounts[acct].locks.length; i > 1; i--) { - unlock(acct, i - 1); + for (uint256 i = accounts[acct].lockIds.length; i > 0; i--) { + unlock(acct, accounts[acct].lockIds[i - 1]); } } function unlockAll(address acct) public { - for (uint256 i = accounts[acct].locks.length; i > 1; i--) { - if (canUnlock(acct, i - 1)) { - unlock(acct, i - 1); + for (uint256 i = accounts[acct].lockIds.length; i > 0; i--) { + uint256 lockId = accounts[acct].lockIds[i - 1]; + if (canUnlock(acct, lockId)) { + unlock(acct, lockId); } } } @@ -271,6 +274,16 @@ contract Staking is ERCStaking, IStaking, AragonApp { } delete(accounts[acct].locks[lockId]); + // delete from lockIds array + // find it first + uint256 locksLength = accounts[acct].lockIds.length; + for (uint256 j = 0; j < locksLength; j++) { + if (accounts[acct].lockIds[j] == lockId) { + break; + } + } + accounts[acct].lockIds[j] = accounts[acct].lockIds[locksLength - 1]; + accounts[acct].lockIds.length--; Unlocked(acct, msg.sender, lockId); } @@ -338,17 +351,15 @@ contract Staking is ERCStaking, IStaking, AragonApp { uint256 unlockedTokens = account.amount; - Lock[] storage locks = accounts[acct].locks; - if (overlocking) { // with ovelocking we only discount biggest one for each unlocker for (uint256 j = 0; j < account.unlockers.length; j++) { if (unlocker == ANY_ENTITY || unlocker != account.unlockers[j].account) { - unlockedTokens = unlockedTokens.sub(locks[account.unlockers[j].maxLockId].amount); + unlockedTokens = unlockedTokens.sub(accounts[acct].locks[account.unlockers[j].maxLockId].amount); } } } else { // without overlocking all locks must be subtracted - for (uint256 i = 1; i < locks.length; i++) { - unlockedTokens = unlockedTokens.sub(locks[i].amount); + for (uint256 i = 0; i < accounts[acct].lockIds.length; i++) { + unlockedTokens = unlockedTokens.sub(accounts[acct].locks[accounts[acct].lockIds[i]].amount); } } @@ -356,7 +367,7 @@ contract Staking is ERCStaking, IStaking, AragonApp { } function locksCount(address acct) public view returns (uint256) { - return accounts[acct].locks.length; + return accounts[acct].lockIds.length; } function getLock( @@ -406,7 +417,7 @@ contract Staking is ERCStaking, IStaking, AragonApp { uint256 nextUnlockerLockId ) { - Lock memory acctLock = accounts[acct].locks[locksCount(acct) - 1]; + Lock memory acctLock = accounts[acct].locks[accounts[acct].lockIds[locksCount(acct) - 1]]; return ( acctLock.amount, diff --git a/future-apps/staking/test/staking.js b/future-apps/staking/test/staking.js index 7c74efbaf2..82e6379358 100644 --- a/future-apps/staking/test/staking.js +++ b/future-apps/staking/test/staking.js @@ -128,7 +128,7 @@ contract('Staking app, no overlocking', accounts => { assert.isFalse(await app.canUnlock.call(owner, lockId)) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount / 2, "Unlocked balance should match for unlocker") - assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10), "last lock id should match") }) it('locks using seconds', async () => { @@ -153,7 +153,7 @@ contract('Staking app, no overlocking', accounts => { assert.isFalse(await app.canUnlock.call(owner, lockId)) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount / 2, "Unlocked balance should match for unlocker") - assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10), "last lock id should match") await app.setTimestamp(endTime + 1) // can unlock, but still locked @@ -185,7 +185,7 @@ contract('Staking app, no overlocking', accounts => { assert.isFalse(await app.canUnlock.call(owner, lockId)) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount / 2, "Unlocked balance should match for unlocker") - assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10), "last lock id should match") await app.setBlockNumber(endBlock + 1) // can unlock @@ -229,7 +229,7 @@ contract('Staking app, no overlocking', accounts => { assert.isFalse(await app.canUnlock.call(owner, lockId)) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), 0, "Unlocked balance should match") assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), 0, "Unlocked balance should match for unlocker") - assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10), "last lock id should match") await app.setTimestamp(endTime + 1) // can unlock @@ -247,7 +247,7 @@ contract('Staking app, no overlocking', accounts => { await app.unlock(owner, lockId, { from: other }) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount, "Unlocked balance should match") - // TODO assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there shouldn't be locks") // lock Id 0 is left empty + assert.equal((await app.locksCount.call(owner)).valueOf(), 0, "there shouldn't be locks") }) it('unlocks non-last lock', async () => { @@ -262,7 +262,7 @@ contract('Staking app, no overlocking', accounts => { await app.unlock(owner, lockId, { from: other }) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") - // TODO assert.equal((await app.locksCount.call(owner)).valueOf(), 2, "there should be just 1 lock") // lock Id 0 is left empty + assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there should be just 1 lock") }) it('unlocks non-last unlocker lock', async () => { @@ -277,7 +277,7 @@ contract('Staking app, no overlocking', accounts => { await app.unlock(owner, lockId, { from: other }) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") - // TODO assert.equal((await app.locksCount.call(owner)).valueOf(), 2, "there should be just 1 lock") // lock Id 0 is left empty + assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there should be just 1 lock") }) it('fails to unlock if can not unlock', async () => { @@ -301,17 +301,10 @@ contract('Staking app, no overlocking', accounts => { await app.lockNow(amount / 4, TIME_UNIT_SECONDS, endTime, other, '', '') // unlock - // TODO await app.unlockAll(owner, { from: other }) - /* - await app.unlock(owner, 2, { from: other }) - console.log('first unlcoked') - await app.unlock(owner, 1, { from: other }) - console.log('second unlcoked') - */ assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount, "Unlocked balance should match") - // TODO assert.equal((await app.locksCount.call(owner)).valueOf(), 0, "there shouldn't be locks") // lock Id 0 is left empty + assert.equal((await app.locksCount.call(owner)).valueOf(), 0, "there shouldn't be locks") }) it('unlocks all with no previous locks', async () => { @@ -331,7 +324,7 @@ contract('Staking app, no overlocking', accounts => { await app.unlockAll(owner) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") - // TODO assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there shouldn't be locks") + assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there should be 1 lock") }) it('fails trying to unlockAllOrNone if a lock cannot be unlocked', async () => { @@ -359,13 +352,13 @@ contract('Staking app, no overlocking', accounts => { await app.unlockPartial(owner, lockId, amount / 4, { from: other }) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount * 3 / 4, "Unlocked balance should match") - assert.equal((await app.locksCount.call(owner)).valueOf(), 2, "there should still be 1 lock") // lock Id 0 is left empty + assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there should still be 1 lock") // unlocks again await app.unlockPartial(owner, lockId, amount / 4, { from: other }) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount, "Unlocked balance should match") - // TODO assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there shouldn't be locks") // lock Id 0 is left empty + assert.equal((await app.locksCount.call(owner)).valueOf(), 0, "there shouldn't be locks") }) it('fails to unlock partially if can not unlock', async () => { @@ -411,7 +404,6 @@ contract('Staking app, no overlocking', accounts => { }) }) - // TODO: should we allow this? Or force to unlock first? Or unlock automatically? it('fails moving unlocked tokens even if they can be unlocked', async () => { const time = 1000 const endTime = (await app.getTimestampExt.call()) + time @@ -445,7 +437,7 @@ contract('Staking app, no overlocking', accounts => { await app.unlockPartialAndMoveTokens(owner, lockId, other, amount / 4, { from: other }) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked owner balance should match") - assert.equal((await app.locksCount.call(owner)).valueOf(), 2, "there should still be 1 lock") // lock Id 0 is left empty + assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there should still be 1 lock") assert.equal((await app.unlockedBalanceOf.call(other, ANY_ENTITY)).valueOf(), amount / 4, "Unlocked other balance should match") }) @@ -538,7 +530,7 @@ contract('Staking app, no overlocking', accounts => { // still the same, can not unlock assert.isFalse(await app.canUnlock.call(owner, lockId)) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") - assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10), "last lock id should match") assert.equal(await executionTarget.lockCounter(), 1, 'should have received execution call') }) @@ -555,7 +547,7 @@ contract('Staking app, no overlocking', accounts => { // can not unlock assert.isFalse(await app.canUnlock.call(owner, lockId)) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), 0, "Unlocked balance should match") - assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10), "last lock id should match") await app.setTimestamp(endTime + 1) // can unlock diff --git a/future-apps/staking/test/staking_no_mock.js b/future-apps/staking/test/staking_no_mock.js index 59203ccb56..52186ea0c9 100644 --- a/future-apps/staking/test/staking_no_mock.js +++ b/future-apps/staking/test/staking_no_mock.js @@ -47,7 +47,7 @@ contract('Staking app, Real one (no mock)', accounts => { assert.isFalse(await app.canUnlock.call(owner, lockId)) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount / 2, "Unlocked balance should match for unlocker") - assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10), "last lock id should match") }) it('locks using blocks', async () => { @@ -71,6 +71,6 @@ contract('Staking app, Real one (no mock)', accounts => { assert.isFalse(await app.canUnlock.call(owner, lockId)) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount / 2, "Unlocked balance should match for unlocker") - assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10), "last lock id should match") }) }) diff --git a/future-apps/staking/test/staking_overlocking.js b/future-apps/staking/test/staking_overlocking.js index a7784d055d..bbf7640204 100644 --- a/future-apps/staking/test/staking_overlocking.js +++ b/future-apps/staking/test/staking_overlocking.js @@ -77,9 +77,9 @@ contract('Staking app, overlocking', accounts => { // still the same, can not unlock assert.isFalse(await app.canUnlock.call(owner, lockId)) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") - // lock doesn't count for the unlocker as overlocing is enabled + // lock doesn't count for the unlocker as overlocking is enabled assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "Unlocked balance should match for unlocker") - assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10), "last lock id should match") }) it('locks already locked tokens (overlocking)', async () => { @@ -93,9 +93,9 @@ contract('Staking app, overlocking', accounts => { assert.notEqual(lockId1, lockId2, "Lock Ids should be different") // only biggest one counts assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2 - 1, "Unlocked balance should match") - // lock doesn't count for the unlocker as overlocing is enabled + // lock doesn't count for the unlocker as overlocking is enabled assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "Unlocked balance should match for unlocker") - assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId2, 10) + 1, "last lock id should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId2, 10), "last lock id should match") }) it('locks using seconds', async () => { @@ -119,15 +119,15 @@ contract('Staking app, overlocking', accounts => { // can not unlock assert.isFalse(await app.canUnlock.call(owner, lockId)) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") - // lock doesn't count for the unlocker as overlocing is enabled + // lock doesn't count for the unlocker as overlocking is enabled assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "Unlocked balance should match for unlocker") - assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10), "last lock id should match") await app.setTimestamp(endTime + 1) // can unlock assert.isTrue(await app.canUnlock.call(owner, lockId)) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") - // lock doesn't count for the unlocker as overlocing is enabled + // lock doesn't count for the unlocker as overlocking is enabled assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "Unlocked balance should match for unlocker") }) @@ -153,9 +153,9 @@ contract('Staking app, overlocking', accounts => { // can not unlock assert.isFalse(await app.canUnlock.call(owner, lockId)) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked balance should match") - // lock doesn't count for the unlocker as overlocing is enabled + // lock doesn't count for the unlocker as overlocking is enabled assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "Unlocked balance should match for unlocker") - assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10) + 1, "last lock id should match") + assert.equal((await app.locksCount.call(owner)).valueOf(), parseInt(lockId, 10), "last lock id should match") await app.setBlockNumber(endBlock + 1) // can unlock @@ -174,17 +174,17 @@ contract('Staking app, overlocking', accounts => { await app.unlockPartial(owner, lockId, amount / 4, { from: other }) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount * 3 / 4, "Unlocked balance should match") - // lock doesn't count for the unlocker as overlocing is enabled + // lock doesn't count for the unlocker as overlocking is enabled assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "Unlocked balance should match for unlocker") - assert.equal((await app.locksCount.call(owner)).valueOf(), 2, "there should still be 1 lock") // id 0 is left empty + assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there should still be 1 lock") // unlocks again await app.unlockPartial(owner, lockId, amount / 4, { from: other }) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount, "Unlocked balance should match") - // lock doesn't count for the unlocker as overlocing is enabled + // lock doesn't count for the unlocker as overlocking is enabled assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "Unlocked balance should match for unlocker") - // TODO assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there shouldn't be locks") // id 0 is left empty + assert.equal((await app.locksCount.call(owner)).valueOf(), 0, "there shouldn't be locks") }) it('moves tokens', async () => { @@ -240,7 +240,7 @@ contract('Staking app, overlocking', accounts => { await app.unlockPartialAndMoveTokens(owner, lockId, other, amount / 4, { from: other }) assert.equal((await app.unlockedBalanceOf.call(owner, ANY_ENTITY)).valueOf(), amount / 2, "Unlocked owner balance should match") - assert.equal((await app.locksCount.call(owner)).valueOf(), 2, "there should still be 1 lock") // id 0 is left empty + assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there should still be 1 lock") assert.equal((await app.unlockedBalanceOf.call(other, ANY_ENTITY)).valueOf(), amount / 4, "Unlocked other balance should match") }) }) From b7fbaead3ad157db508b98092e8df73b9a6f6627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Thu, 26 Jul 2018 16:49:48 +0200 Subject: [PATCH 4/4] Staking: add missing gas reporter package --- future-apps/staking/package-lock.json | 593 ++++++++++++++++++++++++++ future-apps/staking/package.json | 1 + 2 files changed, 594 insertions(+) diff --git a/future-apps/staking/package-lock.json b/future-apps/staking/package-lock.json index a17d83b301..7334eb42f5 100644 --- a/future-apps/staking/package-lock.json +++ b/future-apps/staking/package-lock.json @@ -46,12 +46,172 @@ "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", "dev": true }, + "@types/concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-OU2+C7X+5Gs42JZzXoto7yOQ0A0=", + "dev": true, + "requires": { + "@types/node": "9.6.23" + } + }, + "@types/form-data": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", + "integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=", + "dev": true, + "requires": { + "@types/node": "9.6.23" + } + }, + "@types/node": { + "version": "9.6.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.23.tgz", + "integrity": "sha512-d2SJJpwkiPudEQ3+9ysANN2Nvz4QJKUPoe/WL5zyQzI0RaEeZWH5K5xjvUIGszTItHQpFPdH+u51f6G/LkS8Cg==", + "dev": true + }, + "@types/qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-mNhVdZHdtKHMMxbqzNK3RzkBcN1cux3AvuCYGTvjEIQT2uheH3eCAyYsbMbh2Bq8nXkeOWs1kyDiF7geWRFQ4Q==", + "dev": true + }, "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", "dev": true }, + "abi-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abi-decoder/-/abi-decoder-1.1.0.tgz", + "integrity": "sha512-nvLArBx0XOJufWyaghMKtIofZDBwEMdVZoqcetQOIe1qYeKZk4+kRYGPEJ5lt7dD3MLulw//amUzOJLM8eW5RA==", + "dev": true, + "requires": { + "babel-core": "6.26.3", + "babel-loader": "6.4.1", + "babel-plugin-add-module-exports": "0.2.1", + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-plugin-transform-runtime": "6.23.0", + "babel-preset-es2015": "6.24.1", + "chai": "3.5.0", + "web3": "0.18.4", + "webpack": "2.7.0" + }, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "enhanced-resolve": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz", + "integrity": "sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "memory-fs": "0.4.1", + "object-assign": "4.1.1", + "tapable": "0.2.8" + } + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1", + "object-assign": "4.1.1" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "tapable": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz", + "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=", + "dev": true + }, + "webpack": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-2.7.0.tgz", + "integrity": "sha512-MjAA0ZqO1ba7ZQJRnoCdbM56mmFpipOPUv/vQpwwfSI42p5PVDdoiuK2AL2FwFUVgT859Jr43bFZXRg/LNsqvg==", + "dev": true, + "requires": { + "acorn": "5.6.1", + "acorn-dynamic-import": "2.0.2", + "ajv": "4.11.8", + "ajv-keywords": "1.5.1", + "async": "2.6.0", + "enhanced-resolve": "3.4.1", + "interpret": "1.1.0", + "json-loader": "0.5.7", + "json5": "0.5.1", + "loader-runner": "2.3.0", + "loader-utils": "0.2.17", + "memory-fs": "0.4.1", + "mkdirp": "0.5.1", + "node-libs-browser": "2.1.0", + "source-map": "0.5.7", + "supports-color": "3.1.2", + "tapable": "0.2.8", + "uglify-js": "2.8.29", + "watchpack": "1.6.0", + "webpack-sources": "1.1.0", + "yargs": "6.6.0" + } + }, + "yargs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", + "dev": true, + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "y18n": "3.2.1", + "yargs-parser": "4.2.1" + } + }, + "yargs-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", + "dev": true, + "requires": { + "camelcase": "3.0.0" + } + } + } + }, "abstract-leveldown": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.6.3.tgz", @@ -501,6 +661,12 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true + }, "asn1": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", @@ -550,6 +716,12 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -888,6 +1060,32 @@ "babel-template": "6.26.0" } }, + "babel-loader": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-6.4.1.tgz", + "integrity": "sha1-CzQRLVsHSKjc2/Uaz2+b1C1QuMo=", + "dev": true, + "requires": { + "find-cache-dir": "0.1.1", + "loader-utils": "0.2.17", + "mkdirp": "0.5.1", + "object-assign": "4.1.1" + }, + "dependencies": { + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1", + "object-assign": "4.1.1" + } + } + } + }, "babel-messages": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", @@ -897,6 +1095,12 @@ "babel-runtime": "6.26.0" } }, + "babel-plugin-add-module-exports": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-0.2.1.tgz", + "integrity": "sha1-mumh9KjcZ/DN7E9K7aHkOl/2XiU=", + "dev": true + }, "babel-plugin-check-es2015-constants": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", @@ -1314,6 +1518,15 @@ "regenerator-transform": "0.10.1" } }, + "babel-plugin-transform-runtime": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", + "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, "babel-plugin-transform-strict-mode": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", @@ -1908,6 +2121,17 @@ "lazy-cache": "1.0.4" } }, + "chai": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", + "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", + "dev": true, + "requires": { + "assertion-error": "1.1.0", + "deep-eql": "0.1.3", + "type-detect": "1.0.0" + } + }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", @@ -2140,6 +2364,50 @@ } } }, + "cli-table3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "dev": true, + "requires": { + "colors": "1.3.0", + "object-assign": "4.1.1", + "string-width": "2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, "cli-truncate": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", @@ -2334,6 +2602,18 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "1.1.0", + "inherits": "2.0.3", + "readable-stream": "2.3.4", + "typedarray": "0.0.6" + } + }, "console-browserify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", @@ -2545,6 +2825,23 @@ "mimic-response": "1.0.0" } }, + "deep-eql": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "requires": { + "type-detect": "0.1.1" + }, + "dependencies": { + "type-detect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "dev": true + } + } + }, "deep-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", @@ -3033,6 +3330,130 @@ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", "dev": true }, + "eth-gas-reporter": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/eth-gas-reporter/-/eth-gas-reporter-0.1.10.tgz", + "integrity": "sha512-esVDZWlS+4ByrbHGYHA2sJSevmqBsh7tLTnp7qlc6oJrWqU+SakyTUtHWsUcxhv1SerWEvUl0aRmeytspunmww==", + "dev": true, + "requires": { + "abi-decoder": "1.1.0", + "cli-table3": "0.5.1", + "colors": "1.3.0", + "lodash": "4.17.5", + "mocha": "4.1.0", + "req-cwd": "2.0.0", + "request": "2.83.0", + "request-promise-native": "1.0.5", + "shelljs": "0.7.8", + "solidity-parser-antlr": "0.2.15", + "sync-request": "6.0.0" + }, + "dependencies": { + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "diff": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", + "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "growl": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "dev": true + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "mocha": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", + "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.3.1", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + } + }, + "req-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/req-cwd/-/req-cwd-2.0.0.tgz", + "integrity": "sha1-1AgrTURZgDZkD7c93qAe1T20nrw=", + "dev": true, + "requires": { + "req-from": "2.0.0" + } + }, + "req-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/req-from/-/req-from-2.0.0.tgz", + "integrity": "sha1-10GI5H+TeW9Kpx327jWuaJ8+DnA=", + "dev": true, + "requires": { + "resolve-from": "3.0.0" + } + }, + "shelljs": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", + "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", + "dev": true, + "requires": { + "glob": "7.1.2", + "interpret": "1.1.0", + "rechoir": "0.6.2" + } + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, "ethereum-common": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.2.0.tgz", @@ -3726,6 +4147,28 @@ "repeat-string": "1.6.1" } }, + "find-cache-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", + "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", + "dev": true, + "requires": { + "commondir": "1.0.1", + "mkdirp": "0.5.1", + "pkg-dir": "1.0.0" + }, + "dependencies": { + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true, + "requires": { + "find-up": "1.1.2" + } + } + } + }, "find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", @@ -4400,6 +4843,12 @@ "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", "dev": true }, + "get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=", + "dev": true + }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -4936,12 +5385,35 @@ "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", "dev": true }, + "http-basic": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-7.0.0.tgz", + "integrity": "sha1-gvClBr6UJzLsje6+6A50bvVzbbo=", + "dev": true, + "requires": { + "@types/concat-stream": "1.6.0", + "@types/node": "9.6.23", + "caseless": "0.12.0", + "concat-stream": "1.6.2", + "http-response-object": "3.0.1", + "parse-cache-control": "1.0.1" + } + }, "http-cache-semantics": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", "dev": true }, + "http-response-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.1.tgz", + "integrity": "sha512-6L0Fkd6TozA8kFSfh9Widst0wfza3U1Ex2RjJ6zNDK0vR1U1auUR6jY4Nn2Xl7CCy0ikFmxW1XcspVpb9RvwTg==", + "dev": true, + "requires": { + "@types/node": "9.6.23" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -5661,6 +6133,15 @@ "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", "dev": true }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "0.0.0" + } + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -5688,6 +6169,12 @@ "graceful-fs": "4.1.11" } }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -7249,6 +7736,12 @@ "pbkdf2": "3.0.14" } }, + "parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104=", + "dev": true + }, "parse-glob": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", @@ -7462,6 +7955,15 @@ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true }, + "promise": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.0.1.tgz", + "integrity": "sha1-5F1osAoXZHttpxG/he1u1HII9FA=", + "dev": true, + "requires": { + "asap": "2.0.6" + } + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -7835,6 +8337,26 @@ } } }, + "request-promise-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "dev": true, + "requires": { + "lodash": "4.17.5" + } + }, + "request-promise-native": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", + "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", + "dev": true, + "requires": { + "request-promise-core": "1.1.1", + "stealthy-require": "1.1.1", + "tough-cookie": "2.3.3" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8356,6 +8878,12 @@ } } }, + "solidity-parser-antlr": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/solidity-parser-antlr/-/solidity-parser-antlr-0.2.15.tgz", + "integrity": "sha512-EzRI8/TR/ljTXkZAyAjb0w4G20wH2XM7pcNf87ifdV825AbUv7DkY7HmiZCTj6NeKtIx8Y1s0NZWPbj+JTp8Zw==", + "dev": true + }, "solidity-parser-sc": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/solidity-parser-sc/-/solidity-parser-sc-0.4.1.tgz", @@ -8825,6 +9353,12 @@ } } }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, "stream-browserify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", @@ -8984,6 +9518,26 @@ "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", "dev": true }, + "sync-request": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.0.0.tgz", + "integrity": "sha512-jGNIAlCi9iU4X3Dm4oQnNQshDD3h0/1A7r79LyqjbjUnj69sX6mShAXlhRXgImsfVKtTcnra1jfzabdZvp+Lmw==", + "dev": true, + "requires": { + "http-response-object": "3.0.1", + "sync-rpc": "1.3.4", + "then-request": "6.0.0" + } + }, + "sync-rpc": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.4.tgz", + "integrity": "sha512-Iug+t1ICVFenUcTnDu8WXFnT+k8IVoLKGh8VA3eXUtl2Rt9SjKX3YEv33OenABqpTPL9QEaHv1+CNn2LK8vMow==", + "dev": true, + "requires": { + "get-port": "3.2.0" + } + }, "tapable": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz", @@ -9063,6 +9617,33 @@ "integrity": "sha512-j5EMxnryTvKxwH2Cq+Pb43tsf6sdEgw6Pdwxk83mPaq0ToeFJt6WE4J3s5BqY7vmjlLgkgXvhtXUxo80FyBhCA==", "dev": true }, + "then-request": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.0.tgz", + "integrity": "sha512-xA+7uEMc+jsQIoyySJ93Ad08Kuqnik7u6jLS5hR91Z3smAoCfL3M8/MqMlobAa9gzBfO9pA88A/AntfepkkMJQ==", + "dev": true, + "requires": { + "@types/concat-stream": "1.6.0", + "@types/form-data": "0.0.33", + "@types/node": "8.10.21", + "@types/qs": "6.5.1", + "caseless": "0.12.0", + "concat-stream": "1.6.2", + "form-data": "2.3.2", + "http-basic": "7.0.0", + "http-response-object": "3.0.1", + "promise": "8.0.1", + "qs": "6.5.1" + }, + "dependencies": { + "@types/node": { + "version": "8.10.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.21.tgz", + "integrity": "sha512-87XkD9qDXm8fIax+5y7drx84cXsu34ZZqfB7Cial3Q/2lxSoJ/+DRaWckkCbxP41wFSIrrb939VhzaNxj4eY1w==", + "dev": true + } + } + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -9244,6 +9825,18 @@ "prelude-ls": "1.1.2" } }, + "type-detect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", + "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", diff --git a/future-apps/staking/package.json b/future-apps/staking/package.json index 80b6674594..0a959227f0 100644 --- a/future-apps/staking/package.json +++ b/future-apps/staking/package.json @@ -15,6 +15,7 @@ "license": "GPL-3.0", "devDependencies": { "@aragon/test-helpers": "^1.0.1", + "eth-gas-reporter": "^0.1.1", "ethereumjs-abi": "^0.6.4", "ganache-cli": "^6.0.3", "solidity-coverage": "0.4.3",