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/2] 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 73b7420d1e2ee02e670601589084cf11f3ef29ab 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/2] 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 | 80 ++++-- 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 | 200 +++++++------- .../staking/test/staking_failing_token.js | 33 +++ future-apps/staking/test/staking_no_mock.js | 71 +++++ .../staking/test/staking_overlocking.js | 257 ++++++++++++++++++ 10 files changed, 549 insertions(+), 145 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..ebd5a941a4 100644 --- a/future-apps/staking/contracts/Staking.sol +++ b/future-apps/staking/contracts/Staking.sol @@ -13,6 +13,7 @@ contract Staking is ERCStaking, IStaking, AragonApp { using SafeMath for uint256; uint64 constant public MAX_UINT64 = uint64(-1); + address constant public ANY_ENTITY = address(0); struct Account { uint256 amount; @@ -27,14 +28,15 @@ contract Staking is ERCStaking, IStaking, AragonApp { } 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 +56,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 +93,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,26 +109,45 @@ 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 startin locks in the past? + // require(lockStarts >= (TimeUnit(lockUnit) == TimeUnit.Blocks ? getBlocknumber() : getTimestamp())); + + Lock memory newLock = Lock(amount, Timespan(lockStarts, lockEnds, TimeUnit(lockUnit)), unlocker, metadata); lockId = accounts[msg.sender].locks.push(newLock) - 1; Locked(msg.sender, lockId, amount, metadata); @@ -138,6 +160,7 @@ contract Staking is ERCStaking, IStaking, AragonApp { function stakeAndLock( uint256 amount, uint8 lockUnit, + uint64 lockStarts, uint64 lockEnds, address unlocker, bytes32 metadata, @@ -150,7 +173,7 @@ 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 { @@ -186,11 +209,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,12 +258,25 @@ contract Staking is ERCStaking, IStaking, AragonApp { } function unlockedBalanceOf(address acct) public view returns (uint256) { + return unlockedBalanceOf(acct, ANY_ENTITY); + } + + function unlockedBalanceOf(address acct, address unlocker) public view returns (uint256) { uint256 unlockedTokens = accounts[acct].amount; Lock[] storage locks = accounts[acct].locks; for (uint256 i = 0; i < locks.length; i++) { if (!canUnlock(acct, i)) { - unlockedTokens = unlockedTokens.sub(locks[i].amount); + if (overlocking) { // with ovelocking underflow is possible + if (locks[i].unlocker == ANY_ENTITY || locks[i].unlocker != unlocker) { + if (locks[i].amount > unlockedTokens) { + return 0; + } + unlockedTokens -= locks[i].amount; + } + } else { // without overlocking locks must be always subtracted and no underflow is allowed + unlockedTokens = unlockedTokens.sub(locks[i].amount); + } } } @@ -265,21 +301,21 @@ contract Staking is ERCStaking, IStaking, AragonApp { 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/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..0d9de306b3 100644 --- a/future-apps/staking/test/staking.js +++ b/future-apps/staking/test/staking.js @@ -4,42 +4,45 @@ 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 balance = 1000 + const amount = 100 const zeroBytes32 = "0x0000000000000000000000000000000000000000000000000000000000000000" + const zeroAddress = "0x00" const TIME_UNIT_BLOCKS = 0 const TIME_UNIT_SECONDS = 1 + const OVERLOCKING = false context('Without scripts', async () => { beforeEach(async () => { app = await getContract('StakingMock').new() 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 +55,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 +62,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 +86,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 +102,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 +109,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 +116,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,17 +124,16 @@ 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, zeroAddress)).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 r = await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') const lockId = getEvent(r, 'Locked', 'lockId') // check lock values @@ -158,24 +146,24 @@ contract('Staking app', accounts => { // 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, zeroAddress)).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 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, zeroAddress)).valueOf(), amount, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, other)).valueOf(), amount, "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 endBlock = (await app.getBlockNumberExt.call()) + blocks + const r = await app.lockNow(amount / 2, TIME_UNIT_BLOCKS, endBlock, other, '', '') const lockId = getEvent(r, 'Locked', 'lockId') // check lock values @@ -188,7 +176,8 @@ contract('Staking app', accounts => { // 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, zeroAddress)).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 +186,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, zeroAddress)).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 +230,39 @@ 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.unlockedBalanceOf.call(owner, zeroAddress)).valueOf(), amount, "Unlocked balance should match") assert.equal((await app.locksCount.call(owner)).valueOf(), 0, "there shouldn't be locks") }) 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.unlockedBalanceOf.call(owner, zeroAddress)).valueOf(), amount / 2, "Unlocked balance should match") assert.equal((await app.locksCount.call(owner)).valueOf(), 1, "there should be just 1 lock") }) 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,18 +272,16 @@ 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 await app.unlockAll(owner, { from: other }) - assert.equal((await app.unlockedBalanceOf.call(owner)).valueOf(), amount, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, zeroAddress)).valueOf(), amount, "Unlocked balance should match") assert.equal((await app.locksCount.call(owner)).valueOf(), 0, "there shouldn't be locks") }) @@ -306,30 +291,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.unlockedBalanceOf.call(owner, zeroAddress)).valueOf(), amount / 2, "Unlocked balance should match") 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 +320,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.unlockedBalanceOf.call(owner, zeroAddress)).valueOf(), amount * 3 / 4, "Unlocked balance should match") 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)).valueOf(), amount, "Unlocked balance should match") + assert.equal((await app.unlockedBalanceOf.call(owner, zeroAddress)).valueOf(), amount, "Unlocked balance should match") assert.equal((await app.locksCount.call(owner)).valueOf(), 0, "there shouldnt be locks") }) + 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, zeroAddress)).valueOf(), amount / 2, "Unlocked owner balance should match") + assert.equal((await app.unlockedBalanceOf.call(other, zeroAddress)).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 +368,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 +375,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 +382,35 @@ contract('Staking app', accounts => { }) }) + // TODO: should we allow this? Or force to unlock first? Or unlock automatically? + it('moves unlocked tokens if they can be unlocked', async () => { + const time = 1000 + const endTime = (await app.getTimestampExt.call()) + time + await app.stake(amount, '') + // lock + await app.lockNow(amount / 2, TIME_UNIT_SECONDS, endTime, other, '', '') + // wait for time to go by + await app.setTimestamp(endTime + 1) + // move + await app.moveTokens(owner, other, amount / 2 + 1) + // checks + assert.equal((await app.unlockedBalanceOf.call(owner, zeroAddress)).valueOf(), amount / 2 - 1, "Unlocked owner balance should match") + assert.equal((await app.unlockedBalanceOf.call(other, zeroAddress)).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.unlockedBalanceOf.call(owner, zeroAddress)).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(other, zeroAddress)).valueOf(), amount / 4, "Unlocked other balance should match") }) }) @@ -450,7 +448,10 @@ 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) + 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 +460,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 +475,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 +492,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 +500,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, zeroAddress)).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, zeroAddress)).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 +536,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..4b73779e32 --- /dev/null +++ b/future-apps/staking/test/staking_no_mock.js @@ -0,0 +1,71 @@ +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" + const zeroAddress = "0x00" + const TIME_UNIT_BLOCKS = 0 + const TIME_UNIT_SECONDS = 1 + const OVERLOCKING = false + + beforeEach(async () => { + app = await getContract('Staking').new() + 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 endTime = (await getTimestamp()) + 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") + + // can not unlock + assert.isFalse(await app.canUnlock.call(owner, lockId)) + assert.equal((await app.unlockedBalanceOf.call(owner, zeroAddress)).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 endBlock = web3.eth.blockNumber + 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") + + // can not unlock + assert.isFalse(await app.canUnlock.call(owner, lockId)) + assert.equal((await app.unlockedBalanceOf.call(owner, zeroAddress)).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..95f859c332 --- /dev/null +++ b/future-apps/staking/test/staking_overlocking.js @@ -0,0 +1,257 @@ +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" + const zeroAddress = "0x00" + const TIME_UNIT_BLOCKS = 0 + const TIME_UNIT_SECONDS = 1 + const OVERLOCKING = true + + beforeEach(async () => { + app = await getContract('StakingMock').new() + token = await getContract('StandardTokenMock').new(owner, balance) + 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 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, '') + // 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 () => { + 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') + // 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, zeroAddress)).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 () => { + const amount = 100 + await token.approve(app.address, amount) + await app.stake(amount, '') + // lock twice + const r1 = await app.lockIndefinitely(amount / 2 + 1, 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") + assert.equal((await app.unlockedBalanceOf.call(owner, zeroAddress)).valueOf(), 0, "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 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.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") + + // can not unlock + assert.isFalse(await app.canUnlock.call(owner, lockId)) + assert.equal((await app.unlockedBalanceOf.call(owner, zeroAddress)).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)) + // unlockable balance counts as unlocked + assert.equal((await app.unlockedBalanceOf.call(owner, zeroAddress)).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") + + }) + + 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.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") + + // can not unlock + assert.isFalse(await app.canUnlock.call(owner, lockId)) + assert.equal((await app.unlockedBalanceOf.call(owner, zeroAddress)).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 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.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, zeroAddress)).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(), 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, zeroAddress)).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") + assert.equal((await app.locksCount.call(owner)).valueOf(), 0, "there shouldnt be locks") + }) + + 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, zeroAddress)).valueOf(), amount / 2, "Unlocked owner balance should match") + assert.equal((await app.unlockedBalanceOf.call(other, zeroAddress)).valueOf(), amount / 2, "Unlocked other balance should match") + }) + + 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 () => { + await app.moveTokens(owner, other, amount / 2 + 1) + }) + }) + + it('moves tokens after overlocking', async () => { + const amount = 100 + + // allow Staking app to move owner tokens + await token.approve(app.address, amount) + // 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, zeroAddress)).valueOf(), 0, "Unlocked owner balance should match") + assert.equal((await app.unlockedBalanceOf.call(other, zeroAddress)).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 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.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, zeroAddress)).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, zeroAddress)).valueOf(), amount / 4, "Unlocked other balance should match") + }) +})