Skip to content

Commit

Permalink
Staking (#1)
Browse files Browse the repository at this point in the history
* WIP: Staking

* Add Truffle

* Lint

* Increase coverage to 100%

* Convert ternary conditions into if-else clauses

Because solidity-coverage don't support them.

* Add Echidna scripts

* Fix echidna config

* Remove unnecessary checks commented out

* Change locksCount parameter name for consistency

* Add function headers to Staking

* Remove unnecessary variable from locking test

* Fix function order

* Fix embark test script at package.json

* Remove comment from Checkpointing

* Optimize gas in checkpointing library

* Add tests to echidna

* Change error name and description

* Check that staking token is a contract

* Change functions order

* Change stake and unstake inner logic order

To avoid reentrancy. Although staking token should be safe, it's
better to have it this way. Besides, it's better to have require first too.

* Add Transferred event

* Modify comments

* Add test for canUnlock with an EOA manager

* Remove unnecessary check from transferFromLock function

* Unlock lock if after transfer from it amount goes down to zero

For consistency with check that amount is not zero when creating a
lock. Besides it will avoid having 'garbage' in active lock ids array,
which would make operations like checking unlocked balance more expensive.

* Add Echidna tests

* Clean locking.js

* Add temporary README

* Change default amounts to 120

* Add `truffleit` script to `npm run test:truffle`

* Add conversions to `truffleit` script

* Rename stakeHistory to stakedHistory for consistency

With totalStakedHistory mainly.

* Add Manticore instructions to README

* Update contracts/Checkpointing.sol

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/Checkpointing.sol

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/Staking.sol

Comments

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/Staking.sol

Comments

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/ERCStaking.sol

Comments

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/Staking.sol

Comments

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/Staking.sol

Comments

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/Staking.sol

Comments

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/Staking.sol

Comments

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/Staking.sol

Comments

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/Staking.sol

Comments

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/Staking.sol

Comments

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/Staking.sol

Comments

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/Staking.sol

Comments

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/Staking.sol

Comments

Co-Authored-By: bingen <bingentxu@gmail.com>

* Update contracts/Staking.sol

Comments

Co-Authored-By: bingen <bingentxu@gmail.com>

* Move TimeLockManager to lock-managers folder

* Add IStakingLocking interface

* Minor changes

Addressing PR #1 comments.

* Account only for properly staked balance

See: #1 (comment)

* Rearrange function order and other minor changes

Address PR #1 comments.

* Optimize _unlock function

* Fix test, wrong staking address

* Update contracts/Staking.sol

Add space after `mapping`

Co-Authored-By: bingen <bingentxu@gmail.com>

* Rename event

* Fix checkpointing test using BN

* Move packages from devDependencies to dependencies

* Fix weird issue with embark dependency

If placed inside devDependencies, it complains saying "could not find
specified in dependencies or devdependencies but was found in
node_modules relative to that file"

* Update test_embark/locking.js

Co-Authored-By: bingen <bingentxu@gmail.com>

* Apply suggestions from code review

Cosmetic changes for tests

Co-Authored-By: bingen <bingentxu@gmail.com>

* Cosmetic change to contracts/Staking.sol

Co-Authored-By: bingen <bingentxu@gmail.com>

* Cosmetic changes to tests

Address PR#1 comments.

* Add functions to interface

* Apply suggestions from code review

Co-Authored-By: bingen <bingentxu@gmail.com>

* Add Embark blockchain connector

See: https://embark.status.im/docs/javascript_usage.html#Installing-a-blockchain-connector

* Optimize some gas in checkpointing library?? (#4)

* Optimize some gas in checkpointing library

* Address PR #4 comments

* Address PR #1 comments

* Switch from embark to Truffle

* Address PR #1 comments

- Add getLatestValue for Checkpointing
- Optimize Checkpointing binary search
- Add max locks per account
- Move stakedhistory into Account struct

* chore: update .gitignore

* chore: update readme

* Address PR #1 comments

* Address PR #1 comments
  • Loading branch information
bingen committed Apr 30, 2019
1 parent 5f9faaa commit 0840eac
Show file tree
Hide file tree
Showing 36 changed files with 3,112 additions and 7 deletions.
19 changes: 12 additions & 7 deletions .gitignore
@@ -1,8 +1,13 @@
.embark
chains.json
config/production/password
config/livenet/password
coverage
dist
# dependencies
node_modules
build

# coverage
coverage
coverage.json

# lock files
package-lock.json

# build artifacts
build
flattened_contracts
7 changes: 7 additions & 0 deletions .solcover.js
@@ -0,0 +1,7 @@
module.exports = {
copyPackages: ['@aragon/os'],
testCommand: 'truffle test test_truffle/*.js --network coverage',
skipFiles: [
'test/',
]
}
1 change: 1 addition & 0 deletions .soliumignore
@@ -0,0 +1 @@

23 changes: 23 additions & 0 deletions .soliumrc.json
@@ -0,0 +1,23 @@
{
"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"],
"error-reason": 0
}
}
35 changes: 35 additions & 0 deletions README.md
@@ -0,0 +1,35 @@
# Staking App

A Staking app with checkpointing (implementing ERC900 interface with history) and locking.

## Testing
### Truffle

Currently this app is using Truffle. You can run tests with `npm test`.

### Slither
[Install slither](https://github.com/trailofbits/slither#how-to-install) and then:
```
slither --solc /usr/local/bin/solc .
```

Some noise can be filtered with:
```
slither --solc /usr/local/bin/solc . 2>/tmp/a.txt ; grep -v "is not in mixedCase" /tmp/a.txt | grep "Contract: Staking"
```

### Echidna
Run `./scripts/flatten_echidna.sh` and then:
```
docker run -v `pwd`:/src trailofbits/echidna echidna-test /src/flattened_contracts/EchidnaStaking.sol EchidnaStaking --config="/src/echidna/config.yaml"
```

### Manticore
```
docker run --rm -ti -v `pwd`:/src trailofbits/manticore bash
ulimit -s unlimited
manticore --detect-all --contract Staking /src/flattened_contracts/Staking.sol
```

## Coverage
You can measure coverage using Truffle by running `npm run coverage`.
119 changes: 119 additions & 0 deletions contracts/Checkpointing.sol
@@ -0,0 +1,119 @@
pragma solidity ^0.4.24;


library Checkpointing {
uint256 private constant MAX_UINT192 = uint256(uint192(-1));
uint256 private constant MAX_UINT64 = uint256(uint64(-1));

string private constant ERROR_PAST_CHECKPOINT = "CHECKPOINT_PAST_CHECKPOINT";
string private constant ERROR_TIME_TOO_BIG = "CHECKPOINT_TIME_TOO_BIG";
string private constant ERROR_VALUE_TOO_BIG = "CHECKPOINT_VALUE_TOO_BIG";

struct Checkpoint {
uint64 time;
uint192 value;
}

struct History {
Checkpoint[] history;
}

function add(History storage self, uint256 time, uint256 value) internal {
require(time <= MAX_UINT64, ERROR_TIME_TOO_BIG);
require(value <= MAX_UINT192, ERROR_VALUE_TOO_BIG);

add192(self, uint64(time), uint192(value));
}

function add64(History storage self, uint64 time, uint256 value) internal {
require(value <= MAX_UINT192, ERROR_VALUE_TOO_BIG);

add192(self, time, uint192(value));
}

function get(History storage self, uint256 time) internal view returns (uint256) {
require(time <= MAX_UINT64, ERROR_TIME_TOO_BIG);

return uint256(get192(self, uint64(time)));
}

function get64(History storage self, uint64 time) internal view returns (uint256) {
return uint256(get192(self, time));
}

function lastUpdated(History storage self) internal view returns (uint256) {
uint256 length = self.history.length;

if (length > 0) {
return uint256(self.history[length - 1].time);
}

return 0;
}

function getLatestValue(History storage self) internal view returns (uint256) {
uint256 length = self.history.length;
if (length > 0) {
return uint256(self.history[length - 1].value);
}

return 0;
}

function add192(History storage self, uint64 time, uint192 value) internal {
uint256 length = self.history.length;

if (length == 0) {
self.history.push(Checkpoint(time, value));
} else {
Checkpoint storage currentCheckpoint = self.history[length - 1];
uint64 currentCheckpointTime = currentCheckpoint.time;
if (time > currentCheckpointTime) {
self.history.push(Checkpoint(time, value));
} else if (time == currentCheckpointTime) {
currentCheckpoint.value = value;
} else { // ensure list ordering
revert(ERROR_PAST_CHECKPOINT);
}
}
}

function get192(History storage self, uint64 time) internal view returns (uint192) {
uint256 length = self.history.length;

if (length == 0) {
return 0;
}

uint256 lastIndex = length - 1;

// short-circuit
Checkpoint storage lastCheckpoint = self.history[lastIndex];
if (time >= lastCheckpoint.time) {
return lastCheckpoint.value;
}

if (time < self.history[0].time) {
return 0;
}

uint256 low = 0;
uint256 high = lastIndex;

while (high > low) {
uint256 mid = (high + low + 1) / 2; // average, ceil round
Checkpoint storage checkpoint = self.history[mid];
uint64 midTime = checkpoint.time;

if (time > midTime) {
low = mid;
} else if (time < midTime) {
high = mid - 1;
} else { // time == midTime
return checkpoint.value;
}
}

return self.history[low].value;
}
}
23 changes: 23 additions & 0 deletions contracts/ERCStaking.sol
@@ -0,0 +1,23 @@
pragma solidity ^0.4.24;

// Interface for ERC900: https://eips.ethereum.org/EIPS/eip-900
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) external;
function stakeFor(address user, uint256 amount, bytes data) external;
function unstake(uint256 amount, bytes data) external;

function totalStakedFor(address addr) external view returns (uint256);
function totalStaked() external view returns (uint256);
function token() external view returns (address);

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

interface ERCStakingHistory {
function lastStakedFor(address addr) external view returns (uint256);
function totalStakedForAt(address addr, uint256 blockNumber) external view returns (uint256);
function totalStakedAt(uint256 blockNumber) external view returns (uint256);
}
6 changes: 6 additions & 0 deletions contracts/ILockManager.sol
@@ -0,0 +1,6 @@
pragma solidity ^0.4.24;


interface ILockManager {
function canUnlock(address account, uint256 lockId, bytes lockData) external view returns (bool);
}
33 changes: 33 additions & 0 deletions contracts/IStakingLocking.sol
@@ -0,0 +1,33 @@
pragma solidity ^0.4.24;

import "./ILockManager.sol";


interface IStakingLocking {
event Locked(address indexed account, uint256 indexed lockId, uint256 amount, address manager, bytes data);
event Unlocked(address indexed account, uint256 indexed lockId, uint256 amount, address manager, bytes data);
event LockAmountChanged(address indexed account, uint256 indexed lockId, uint256 amount);
event LockManagerChanged(address indexed account, uint256 indexed lockId, address manager);
event LockDataChanged(address indexed account, uint256 indexed lockId, bytes data);

function lock(uint256 _amount, address _manager, bytes _data) external returns (uint256 _lockId);
function unlock(address _account, uint256 _lockId) external;
function decreaseLockAmount(address _account, uint256 _lockId, uint256 _newAmount) external;
function setLockManager(address _account, uint256 _lockId, ILockManager _newManager) external;
function setLockData(address _account, uint256 _lockId, bytes _newData) external;
function transfer(address _to, uint256 _toLockId, uint256 _amount) external;
function transferFromLock(address _account, uint256 _lockId, address _to, uint256 _toLockId, uint256 _amount) external;

function locksCount(address _account) external view returns (uint256);
function getLock(address _account, uint256 _lockId)
external
view
returns (
uint256 _amount,
uint64 _unlockedAt,
address _manager,
bytes _data
);
function unlockedBalanceOf(address _account) external view returns (uint256);
function canUnlock(address _account, uint256 _lockId) external view returns (bool);
}

0 comments on commit 0840eac

Please sign in to comment.