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 app #101

Closed
wants to merge 21 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
6 changes: 6 additions & 0 deletions future-apps/staking/.solcover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
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',
copyNodeModules: true,
}
22 changes: 22 additions & 0 deletions future-apps/staking/.soliumrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"extends": "solium:all",
"rules": {
"imports-on-top": ["error"],
"variable-declarations": ["error"],
"array-declarations": ["error"],
"operator-whitespace": ["error"],
"lbrace": ["error"],
"mixedcase": 0,
"camelcase": ["error"],
"uppercase": 0,
"no-empty-blocks": ["error"],
"no-unused-vars": ["error"],
"quotes": ["error"],
"indentation": 0,
"whitespace": ["error"],
"deprecated-suicide": ["error"],
"arg-overflow": ["error", 8],
"pragma-on-top": ["error"],
"security/enforce-explicit-visibility": ["error"]
}
}
23 changes: 23 additions & 0 deletions future-apps/staking/contracts/ERCStaking.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
pragma solidity ^0.4.18;


interface ERCStaking {
event Staked(address indexed user, uint256 amount, uint256 total, bytes data);
event Unstaked(address indexed user, uint256 amount, uint256 total, bytes data);

function stake(uint256 amount, bytes data) public;
function stakeFor(address user, uint256 amount, bytes data) public;
function unstake(uint256 amount, bytes data) public;

function totalStakedFor(address addr) public view returns (uint256);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing totalStaked? I saw discussion here and your response, but it seems it's part of the interface definition.

function totalStaked() public view returns (uint256);
function token() public view returns (address);

function supportsHistory() public pure returns (bool);
}


// to avoid coverage issue
contract ERCFake {
function ERCFake() public {}
}
266 changes: 266 additions & 0 deletions future-apps/staking/contracts/Staking.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
pragma solidity 0.4.18;

import "./ERCStaking.sol";

import "@aragon/os/contracts/apps/AragonApp.sol";
import "@aragon/os/contracts/lib/misc/Migrations.sol";
import "@aragon/os/contracts/lib/zeppelin/token/ERC20.sol";
import "@aragon/os/contracts/lib/zeppelin/math/SafeMath.sol";


contract Staking is ERCStaking, AragonApp {
using SafeMath for uint256;

uint64 constant public MAX_UINT64 = uint64(-1);

struct Account {
uint256 amount;
Lock[] locks;
}

struct Lock {
uint256 amount;
Timespan timespan;
address unlocker;
bytes32 metadata;
}

struct Timespan {
//uint64 start; // exclusive
uint64 end; // inclusive
TimeUnit unit;
}

enum TimeUnit { Blocks, Seconds }

ERC20 public stakingToken;
bytes public stakeScript;
bytes public unstakeScript;
bytes public lockScript;

mapping (address => Account) accounts;

// TODO: figure out a way to get lock from metadata, given changing lock ids
// mapping (bytes32 => LockLocation) lockByMetadata;

event Locked(address indexed account, uint256 lockId, uint256 amount, bytes32 metadata);
event Unlocked(address indexed account, address indexed unlocker, uint256 oldLockId);

event MovedTokens(address indexed from, address indexed to, uint256 amount);

bytes32 constant public STAKE_ROLE = keccak256("STAKE_ROLE");
bytes32 constant public UNSTAKE_ROLE = keccak256("UNSTAKE_ROLE");
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);
_;
}

// TODO: Implement forwarder interface

function initialize(ERC20 _stakingToken, bytes _stakeScript, bytes _unstakeScript, bytes _lockScript) onlyInit external {
stakingToken = _stakingToken;
stakeScript = _stakeScript;
unstakeScript = _unstakeScript;
lockScript = _lockScript;

initialized();
}

function stake(uint256 amount, bytes data) authP(STAKE_ROLE, arr(amount)) public {
stakeFor(msg.sender, amount, data);
}

function stakeFor(address acct, uint256 amount, bytes data) authP(STAKE_ROLE, arr(amount)) public {
// stake 0 tokens makes no sense
require(amount > 0);
// From needs to be msg.sender to avoid token stealing by front-running
require(stakingToken.transferFrom(msg.sender, this, amount));

// process Stake
accounts[acct].amount = accounts[acct].amount.add(amount);

Staked(acct, amount, totalStakedFor(acct), data);

if (stakeScript.length > 0) {
runScript(stakeScript, data, new address[](0));
}
}

function unstake(uint256 amount, bytes data) authP(UNSTAKE_ROLE, arr(amount)) checkUnlocked(amount) public {
// unstake 0 tokens makes no sense
require(amount > 0);

accounts[msg.sender].amount = accounts[msg.sender].amount.sub(amount);

require(stakingToken.transfer(msg.sender, amount));

Unstaked(msg.sender, amount, totalStakedFor(msg.sender), data);

if (unstakeScript.length > 0) {
runScript(unstakeScript, data, new address[](0));
}
}

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);
}

function lock(
uint256 amount,
uint8 lockUnit,
uint64 lockEnds,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't be useful a helper/wrapper to indefinitely lock tokens without forcing the user to pass MAX UINT64?

address unlocker,
bytes32 metadata,
bytes data
)
authP(LOCK_ROLE, arr(amount, uint256(lockUnit), uint256(lockEnds)))
checkUnlocked(amount)
public
returns(uint256 lockId)
{
// lock 0 tokens makes no sense
require(amount > 0);

Lock memory newLock = Lock(amount, Timespan(lockEnds, TimeUnit(lockUnit)), unlocker, metadata);
lockId = accounts[msg.sender].locks.push(newLock) - 1;

Locked(msg.sender, lockId, amount, metadata);

if (lockScript.length > 0) {
runScript(lockScript, data, new address[](0));
}
}

function stakeAndLock(
uint256 amount,
uint8 lockUnit,
uint64 lockEnds,
address unlocker,
bytes32 metadata,
bytes stakeData,
bytes lockData
)
authP(STAKE_ROLE, arr(amount))
authP(LOCK_ROLE, arr(amount, uint256(lockUnit), uint256(lockEnds)))
public
returns(uint256 lockId)
{
stake(amount, stakeData);
return lock(amount, lockUnit, lockEnds, unlocker, metadata, lockData);
}

function unlockAllOrNone(address acct) external {
for (uint256 i = accounts[acct].locks.length; i > 0; i--) {
unlock(acct, i - 1);
}
}

function unlockAll(address acct) public {
for (uint256 i = accounts[acct].locks.length; i > 0; i--) {
if (canUnlock(acct, i - 1)) {
unlock(acct, i - 1);
}
}
}

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];
}

accounts[acct].locks.length -= 1;

Unlocked(acct, msg.sender, lockId);
}

function unlockAndUnstake(uint256 amount, bytes data) public {
unlockAll(msg.sender);
unstake(amount, data);
}

function moveTokens(address from, address to, uint256 amount) authP(GOD_ROLE, arr(from, to, amount)) external {
// move 0 tokens makes no sense
require(amount > 0);

// make sure we don't move locked tokens, to avoid inconsistency
require(unlockedBalanceOf(from) >= amount);

accounts[from].amount = accounts[from].amount.sub(amount);
accounts[to].amount = accounts[to].amount.add(amount);

MovedTokens(from, to, amount);
}

// ERCStaking

function token() public view returns (address) {
return stakingToken;
}

function supportsHistory() public pure returns (bool) {
return false;
}

function totalStakedFor(address addr) public view returns (uint256) {
return accounts[addr].amount;
}

function totalStaked() public view returns (uint256) {
return stakingToken.balanceOf(this);
}

function unlockedBalanceOf(address acct) public view returns (uint256) {
uint256 unlockedTokens = accounts[acct].amount;

Lock[] storage locks = accounts[acct].locks;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be memory

for (uint256 i = 0; i < locks.length; i++) {
if (!canUnlock(acct, i)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this. I think I understand the rationale, but I see a potential problem: If unstake function is called, it will call this function to check available staked but not locked tokens. If there are potentially unlocked, but actually locked tokens, they may be unstaked, leaving a bigger total locked balance than the staked balance.

unlockedTokens = unlockedTokens.sub(locks[i].amount);
}
}

return unlockedTokens;
}

function locksCount(address acct) public view returns (uint256) {
return accounts[acct].locks.length;
}

function getLock(address acct, uint256 lockId) public view returns (uint256 amount, uint8 lockUnit, uint64 lockEnds, address unlocker, bytes32 metadata) {
Lock memory acctLock = accounts[acct].locks[lockId];

return (acctLock.amount, uint8(acctLock.timespan.unit), uint64(acctLock.timespan.end), acctLock.unlocker, acctLock.metadata);
}

function lastLock(address acct) public view returns (uint256 amount, uint8 lockUnit, uint64 lockEnds, address unlocker, bytes32 metadata) {
Lock memory acctLock = accounts[acct].locks[locksCount(acct) - 1];
return (acctLock.amount, uint8(acctLock.timespan.unit), uint64(acctLock.timespan.end), acctLock.unlocker, acctLock.metadata);
}

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;
}

function timespanEnded(Timespan memory timespan) internal view returns (bool) {
uint256 comparingValue = timespan.unit == TimeUnit.Blocks ? getBlocknumber() : getTimestamp();

return uint64(comparingValue) > timespan.end;
}

function getTimestamp() internal view returns (uint256) {
return block.timestamp;
}

// TODO: Use getBlockNumber from Initializable.sol - issue with solidity-coverage
function getBlocknumber() internal returns (uint256) {
return block.number;
}
}
5 changes: 5 additions & 0 deletions future-apps/staking/migrations/1_initial_migration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
var Migrations = artifacts.require("./Migrations.sol");

module.exports = function(deployer, n, accounts) {
deployer.deploy(Migrations)
}
Loading