Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Staking 4 deferred lock simple #408

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions future-apps/staking/.solcover.js
Original file line number Diff line number Diff line change
@@ -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,
}
82 changes: 59 additions & 23 deletions future-apps/staking/contracts/Staking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);

Expand All @@ -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);
Expand All @@ -138,6 +160,7 @@ contract Staking is ERCStaking, IStaking, AragonApp {
function stakeAndLock(
uint256 amount,
uint8 lockUnit,
uint64 lockStarts,
uint64 lockEnds,
address unlocker,
bytes32 metadata,
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -211,7 +234,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);
}
Expand All @@ -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);
}
}
}

Expand All @@ -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);
}
}
2 changes: 1 addition & 1 deletion future-apps/staking/contracts/interfaces/IStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions future-apps/staking/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions future-apps/staking/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions future-apps/staking/test/mocks/FailingTokenMock.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
18 changes: 9 additions & 9 deletions future-apps/staking/test/mocks/StakingMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Loading