-
Notifications
You must be signed in to change notification settings - Fork 211
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
Staking app #101
Changes from all commits
dcebfc5
a513d62
fad9be6
c254d05
73bddb9
e9390fd
57ff719
6514851
f70da64
ae92945
4e1e07e
02a5b6c
358b461
dd6eaaf
bfa9fa3
1ddd278
a1c7fd3
ea1ce83
7877e47
99f507f
b8604b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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, | ||
} |
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"] | ||
} | ||
} |
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); | ||
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 {} | ||
} |
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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; | ||
} | ||
} |
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) | ||
} |
There was a problem hiding this comment.
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.