tests: add solidity test suites (#487)

* tests: add solidity test suite

* tests: remove require strings

* Update tests-solidity/init-test-node.sh

* Update tests-solidity/init-test-node.sh

Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com>
This commit is contained in:
Brett Sun 2020-09-01 23:16:28 +02:00 committed by GitHub
parent 4344dc10c7
commit c9639c3860
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 16013 additions and 0 deletions

1
tests-solidity/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.sol linguist-language=Solidity

5
tests-solidity/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# dependencies
node_modules/
# ignore package-lock files (only use yarn.lock)
package-lock.json

102
tests-solidity/README.md Normal file
View File

@ -0,0 +1,102 @@
# Solidity tests
Increasingly difficult tests are provided:
- [Basic](./suites/basic): simple Counter example, for basic calls, transactions, and events
- [Initialize](./suites/initialize): initialization contract and tests from [aragonOS](https://github.com/aragon/aragonOS)
- [Initialize (Buidler)](./suites/initialize-buidler): initialization contract and tests from [aragonOS](https://github.com/aragon/aragonOS), using [buidler](https://buidler.dev/)
- [Proxy](./suites/proxy): depositable delegate proxy contract and tests from [aragonOS](https://github.com/aragon/aragonOS)
- [Staking](./suites/staking): Staking contracts and full test suite from [aragon/staking](http://github.com/aragon/staking)
### Quick start
**Prerequisite**: in the repo's root, run `make install` to install the `ethermintd` and `ethermintcli` binaries. When done, come back to this directory.
**Prerequisite**: install the individual solidity packages. They're set up as individual reops in a yarn monorepo workspace. Install them all via `yarn install`.
To run the tests, start three terminals (or two, if you run `ethermintd` with `&`).
In the first, run `ethermintd`:
```sh
./init-test-node.sh
```
In the second, run `ethermintcli` as mentioned in the script's output:
```sh
ethermintcli rest-server --laddr "tcp://localhost:8545" --unlock-key localkey,user1,user2 --chain-id 1337 --trace --wsport 8546
```
You will now have three ethereum accounts unlocked in the test node:
- `0x3b7252d007059ffc82d16d022da3cbf9992d2f70` (Validator)
- `0xddd64b4712f7c8f1ace3c145c950339eddaf221d` (User 1)
- `0x0f54f47bf9b8e317b214ccd6a7c3e38b893cd7f0` (user 2)
From here, in your other available terminal, go into any of the tests and run `yarn test-ethermint`. You should see `ethermintd` accepting transactions and producing blocks. You should be able to query for any transaction via:
- `ethermintcli query tx <cosmos-sdk tx>`
- `curl localhost:8545 -H "Content-Type:application/json" -X POST --data '{"jsonrpc":"2.0","method":"eth_getTransactionByHash","params":["<ethereum tx>"],"id":1}'`
And obviously more, via the Ethereum JSON-RPC API).
When in doubt, you can also run the tests against a Ganache instance via `yarn test-ganache`, to make sure they are behaving correctly.
### Test node
The [`init-test-node.sh`](./init-test-node.sh) script sets up ethermint with the following accounts:
- `eth18de995q8qk0leqk3d5pzmg7tlxvj6tmsku084d` (Validator)
- `0x3b7252d007059ffc82d16d022da3cbf9992d2f70`
- `eth1mhtyk3cj7ly0rt8rc9zuj5pnnmw67gsapygwyq` (User 1)
- `0xddd64b4712f7c8f1ace3c145c950339eddaf221d`
- `eth1pa20g7lehr330vs5ent20slr3wyne4lsy8qae3` (user 2)
- `0x0f54f47bf9b8e317b214ccd6a7c3e38b893cd7f0`
Each with roughly 100 ETH available (1e18 photon).
Running `ethermintcli list keys` should output:
```json
[
{
"name": "localkey",
"type": "local",
"address": "eth18de995q8qk0leqk3d5pzmg7tlxvj6tmsku084d",
"pubkey": "ethpub1pfqnmk6pq3ycjs34vv4n6rkty89f6m02qcsal3ecdzn7a3uunx0e5ly0846pzg903hxf2zp5gq4grh8jcatcemfrscdfl797zhg5crkcsx43gujzppge3n"
},
{
"name": "user1",
"type": "local",
"address": "eth1mhtyk3cj7ly0rt8rc9zuj5pnnmw67gsapygwyq",
"pubkey": "ethpub1pfqnmk6pq3wrkx6lh7uug8ss0thggact3n49m5gkmpca4vylldpur5qrept57e0rrxfmeq5mp5xt3cyf4kys53qcv66qxttv970das69hlpkf8cnyd2a2x"
},
{
"name": "user2",
"type": "local",
"address": "eth1pa20g7lehr330vs5ent20slr3wyne4lsy8qae3",
"pubkey": "ethpub1pfqnmk6pq3art9y45zw5ntyktt2qrt0skmsl0ux9qwk8458ed3d8sgnrs99zlgvj3rt2vggvkh0x56hffugwsyddwqla48npx46pglgs6xhcqpall58tgn"
}
]
```
And running:
```
curl localhost:8545 -H "Content-Type:application/json" -X POST --data '{"jsonrpc":"2.0","method":"eth_accounts","params":[],"id":1}'
```
Should output:
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": [
"0x3b7252d007059ffc82d16d022da3cbf9992d2f70",
"0xddd64b4712f7c8f1ace3c145c950339eddaf221d",
"0x0f54f47bf9b8e317b214ccd6a7c3e38b893cd7f0"
]
}
```

View File

@ -0,0 +1,61 @@
#!/bin/bash
CHAINID=1337
MONIKER="localtestnet"
VAL_KEY="localkey"
VAL_MNEMONIC="gesture inject test cycle original hollow east ridge hen combine junk child bacon zero hope comfort vacuum milk pitch cage oppose unhappy lunar seat"
USER1_KEY="user1"
USER1_MNEMONIC="copper push brief egg scan entry inform record adjust fossil boss egg comic alien upon aspect dry avoid interest fury window hint race symptom"
USER2_KEY="user2"
USER2_MNEMONIC="maximum display century economy unlock van census kite error heart snow filter midnight usage egg venture cash kick motor survey drastic edge muffin visual"
# remove existing daemon and client
rm -rf ~/.ethermint*
ethermintcli config keyring-backend test
# Set up config for CLI
ethermintcli config chain-id $CHAINID
ethermintcli config output json
ethermintcli config indent true
ethermintcli config trust-node true
# Import keys from mnemonics
echo $VAL_MNEMONIC | ethermintcli keys add $VAL_KEY --recover
echo $USER1_MNEMONIC | ethermintcli keys add $USER1_KEY --recover
echo $USER2_MNEMONIC | ethermintcli keys add $USER2_KEY --recover
# Set moniker and chain-id for Ethermint (Moniker can be anything, chain-id must be an integer)
ethermintd init $MONIKER --chain-id $CHAINID
# Allocate genesis accounts (cosmos formatted addresses)
ethermintd add-genesis-account $(ethermintcli keys show $VAL_KEY -a) 1000000000000000000000aphoton,10000000000000000stake
ethermintd add-genesis-account $(ethermintcli keys show $USER1_KEY -a) 1000000000000000000000aphoton,10000000000000000stake
ethermintd add-genesis-account $(ethermintcli keys show $USER2_KEY -a) 1000000000000000000000aphoton,10000000000000000stake
# Sign genesis transaction
ethermintd gentx --name $VAL_KEY --keyring-backend test
# Collect genesis tx
ethermintd collect-gentxs
# Enable faucet
cat $HOME/.ethermintd/config/genesis.json | jq '.app_state["faucet"]["enable_faucet"]=true' > $HOME/.ethermintd/config/tmp_genesis.json && mv $HOME/.ethermintd/config/tmp_genesis.json $HOME/.ethermintd/config/genesis.json
echo -e '\n\ntestnet faucet enabled'
echo -e 'to transfer tokens to your account address use:'
echo -e "ethermintcli tx faucet request 100aphoton --from $VAL_KEY\n"
# Run this to ensure everything worked and that the genesis file is setup correctly
ethermintd validate-genesis
# Command to run the rest server in a different terminal/window
echo -e '\nrun the following command in a different terminal/window to run the REST server and JSON-RPC:'
echo -e "ethermintcli rest-server --laddr \"tcp://localhost:8545\" --wsport 8546 --unlock-key $VAL_KEY,$USER1_KEY,$USER2_KEY --chain-id $CHAINID --trace\n"
# Start the node (remove the --pruning=nothing flag if historical queries are not needed)
ethermintd start --pruning=nothing --rpc.unsafe --log_level "main:info,state:info,mempool:info" --trace

View File

@ -0,0 +1,15 @@
{
"name": "tests-solidity",
"private": true,
"version": "1.0.0",
"author": "Aragon Association <contact@aragon.org>",
"license": "GPL-3.0-or-later",
"workspaces": {
"packages": [
"suites/*"
],
"nohoist": [
"**/@aragon/contract-helpers-test"
]
}
}

View File

@ -0,0 +1,24 @@
pragma solidity ^0.5.11;
contract Counter {
uint256 counter = 0;
string internal constant ERROR_TOO_LOW = "COUNTER_TOO_LOW";
event Changed(uint256 counter);
event Added(uint256 counter);
function add() public {
counter++;
emit Added(counter);
emit Changed(counter);
}
function subtract() public {
require(counter > 0, ERROR_TOO_LOW);
counter--;
emit Changed(counter);
}
function getCounter() public view returns (uint256) {
return counter;
}
}

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.8.0;
contract Migrations {
address public owner = msg.sender;
uint public last_completed_migration;
modifier restricted() {
require(
msg.sender == owner,
"This function is restricted to the contract's owner"
);
_;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
}

View File

@ -0,0 +1,5 @@
const Migrations = artifacts.require("Migrations");
module.exports = function (deployer) {
deployer.deploy(Migrations);
};

View File

@ -0,0 +1,14 @@
{
"name": "basic",
"version": "1.0.0",
"author": "Aragon Association <contact@aragon.org>",
"license": "GPL-3.0-or-later",
"scripts": {
"test-ganache": "yarn truffle test",
"test-ethermint": "yarn truffle test --network ethermint"
},
"devDependencies": {
"truffle": "^5.1.42",
"web3": "^1.2.11"
}
}

View File

@ -0,0 +1,80 @@
const Counter = artifacts.require("Counter")
contract('Counter', ([one, two, three]) => {
console.log(one, two, three)
let counter
beforeEach(async() => {
counter = await Counter.new()
console.log('Counter:', counter.address)
console.log('Current eth:')
console.log(' - ', await web3.eth.getBalance(one))
console.log(' - ', await web3.eth.getBalance(two))
console.log(' - ', await web3.eth.getBalance(three))
console.log('')
})
it('should add', async() => {
const balanceOne = await web3.eth.getBalance(one)
const balanceTwo = await web3.eth.getBalance(two)
const balanceThree = await web3.eth.getBalance(three)
let count
await counter.add({ from: one })
count = await counter.getCounter()
console.log(count.toString())
assert.equal(count, '1', 'Counter should be 1')
assert.notEqual(balanceOne, await web3.eth.getBalance(one), `${one}'s balance should be different`)
await counter.add({ from: two })
count = await counter.getCounter()
console.log(count.toString())
assert.equal(count, '2', 'Counter should be 2')
assert.notEqual(balanceTwo, await web3.eth.getBalance(two), `${two}'s balance should be different`)
await counter.add({ from: three })
count = await counter.getCounter()
console.log(count.toString())
assert.equal(count, '3', 'Counter should be 3')
assert.notEqual(balanceThree, await web3.eth.getBalance(three), `${three}'s balance should be different`)
})
it('should subtract', async() => {
let count
await counter.add()
count = await counter.getCounter()
console.log(count.toString())
assert.equal(count, '1', 'Counter should be 1')
// Use receipt to ensure logs are emitted
const receipt = await counter.subtract()
count = await counter.getCounter()
console.log(count.toString())
console.log()
console.log('Subtract tx receipt:', receipt)
assert.equal(count, '0', 'Counter should be 0')
assert.equal(receipt.logs[0].event, 'Changed', "Should have emitted 'Changed' event")
assert.equal(receipt.logs[0].args.counter, '0', "Should have emitted 'Changed' event with counter being 0")
// Check lifecycle of events
const contract = new web3.eth.Contract(counter.abi, counter.address)
const allEvents = await contract.getPastEvents("allEvents", { fromBlock: 0, toBlock: 'latest' })
const changedEvents = await contract.getPastEvents("Changed", { fromBlock: 0, toBlock: 'latest' })
console.log('allEvents', allEvents)
console.log('changedEvents', changedEvents)
assert.equal(allEvents.length, 3)
assert.equal(changedEvents.length, 2)
try {
await counter.subtract()
assert.fail('Subtracting past 0 should have reverted')
} catch (err) {
console.log()
console.log('Passed -- was expecting error')
console.log('Error:', err)
}
})
})

View File

@ -0,0 +1,17 @@
module.exports = {
networks: {
// Development network is just left as truffle's default settings
ethermint: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
gas: 5000000, // Gas sent with each transaction
gasPrice: 1000000000, // 1 gwei (in wei)
},
},
compilers: {
solc: {
version: "0.5.17",
},
},
}

View File

@ -0,0 +1,3 @@
# Buidler
artifacts
cache

View File

@ -0,0 +1,28 @@
const { usePlugin } = require('@nomiclabs/buidler/config')
usePlugin("@nomiclabs/buidler-ganache")
usePlugin('@nomiclabs/buidler-truffle5')
module.exports = {
networks: {
// Development network is just left as truffle's default settings
ganache: {
url: 'http://localhost:8545',
gasLimit: 5000000,
gasPrice: 1000000000, // 1 gwei (in wei)
defaultBalanceEther: 100
},
ethermint: {
url: 'http://localhost:8545',
gasLimit: 5000000, // Gas sent with each transaction
gasPrice: 1000000000, // 1 gwei (in wei)
},
},
solc: {
version: '0.4.24',
optimizer: {
enabled: true,
runs: 10000,
},
},
}

View File

@ -0,0 +1,59 @@
/*
* SPDX-License-Identifier: MIT
*/
pragma solidity ^0.4.24;
import "./TimeHelpers.sol";
import "./UnstructuredStorage.sol";
contract Initializable is TimeHelpers {
using UnstructuredStorage for bytes32;
// keccak256("aragonOS.initializable.initializationBlock")
bytes32 internal constant INITIALIZATION_BLOCK_POSITION = 0xebb05b386a8d34882b8711d156f463690983dc47815980fb82aeeff1aa43579e;
string private constant ERROR_ALREADY_INITIALIZED = "INIT_ALREADY_INITIALIZED";
string private constant ERROR_NOT_INITIALIZED = "INIT_NOT_INITIALIZED";
modifier onlyInit {
require(getInitializationBlock() == 0, ERROR_ALREADY_INITIALIZED);
_;
}
modifier isInitialized {
require(hasInitialized(), ERROR_NOT_INITIALIZED);
_;
}
/**
* @return Block number in which the contract was initialized
*/
function getInitializationBlock() public view returns (uint256) {
return INITIALIZATION_BLOCK_POSITION.getStorageUint256();
}
/**
* @return Whether the contract has been initialized by the time of the current block
*/
function hasInitialized() public view returns (bool) {
uint256 initializationBlock = getInitializationBlock();
return initializationBlock != 0 && getBlockNumber() >= initializationBlock;
}
/**
* @dev Function to be called by top level contract after initialization has finished.
*/
function initialized() internal onlyInit {
INITIALIZATION_BLOCK_POSITION.setStorageUint256(getBlockNumber());
}
/**
* @dev Function to be called by top level contract after initialization to enable the contract
* at a future block number rather than immediately.
*/
function initializedAt(uint256 _blockNumber) internal onlyInit {
INITIALIZATION_BLOCK_POSITION.setStorageUint256(_blockNumber);
}
}

View File

@ -0,0 +1,21 @@
pragma solidity ^0.4.24;
import "./Initializable.sol";
contract Petrifiable is Initializable {
// Use block UINT256_MAX (which should be never) as the initializable date
uint256 internal constant PETRIFIED_BLOCK = uint256(-1);
function isPetrified() public view returns (bool) {
return getInitializationBlock() == PETRIFIED_BLOCK;
}
/**
* @dev Function to be called by top level contract to prevent being initialized.
* Useful for freezing base contracts when they're used behind proxies.
*/
function petrify() internal onlyInit {
initializedAt(PETRIFIED_BLOCK);
}
}

View File

@ -0,0 +1,44 @@
pragma solidity ^0.4.24;
import "./Uint256Helpers.sol";
contract TimeHelpers {
using Uint256Helpers for uint256;
/**
* @dev Returns the current block number.
* Using a function rather than `block.number` allows us to easily mock the block number in
* tests.
*/
function getBlockNumber() internal view returns (uint256) {
return block.number;
}
/**
* @dev Returns the current block number, converted to uint64.
* Using a function rather than `block.number` allows us to easily mock the block number in
* tests.
*/
function getBlockNumber64() internal view returns (uint64) {
return getBlockNumber().toUint64();
}
/**
* @dev Returns the current timestamp.
* Using a function rather than `block.timestamp` allows us to easily mock it in
* tests.
*/
function getTimestamp() internal view returns (uint256) {
return block.timestamp; // solium-disable-line security/no-block-members
}
/**
* @dev Returns the current timestamp, converted to uint64.
* Using a function rather than `block.timestamp` allows us to easily mock it in
* tests.
*/
function getTimestamp64() internal view returns (uint64) {
return getTimestamp().toUint64();
}
}

View File

@ -0,0 +1,13 @@
pragma solidity ^0.4.24;
library Uint256Helpers {
uint256 private constant MAX_UINT64 = uint64(-1);
string private constant ERROR_NUMBER_TOO_BIG = "UINT64_NUMBER_TOO_BIG";
function toUint64(uint256 a) internal pure returns (uint64) {
require(a <= MAX_UINT64, ERROR_NUMBER_TOO_BIG);
return uint64(a);
}
}

View File

@ -0,0 +1,36 @@
pragma solidity ^0.4.24;
library UnstructuredStorage {
function getStorageBool(bytes32 position) internal view returns (bool data) {
assembly { data := sload(position) }
}
function getStorageAddress(bytes32 position) internal view returns (address data) {
assembly { data := sload(position) }
}
function getStorageBytes32(bytes32 position) internal view returns (bytes32 data) {
assembly { data := sload(position) }
}
function getStorageUint256(bytes32 position) internal view returns (uint256 data) {
assembly { data := sload(position) }
}
function setStorageBool(bytes32 position, bool data) internal {
assembly { sstore(position, data) }
}
function setStorageAddress(bytes32 position, address data) internal {
assembly { sstore(position, data) }
}
function setStorageBytes32(bytes32 position, bytes32 data) internal {
assembly { sstore(position, data) }
}
function setStorageUint256(bytes32 position, uint256 data) internal {
assembly { sstore(position, data) }
}
}

View File

@ -0,0 +1,15 @@
pragma solidity 0.4.24;
import "../Initializable.sol";
import "../Petrifiable.sol";
contract LifecycleMock is Initializable, Petrifiable {
function initializeMock() public {
initialized();
}
function petrifyMock() public {
petrify();
}
}

View File

@ -0,0 +1,19 @@
{
"name": "initializable-buidler",
"version": "1.0.0",
"author": "Aragon Association <contact@aragon.org>",
"license": "GPL-3.0-or-later",
"scripts": {
"test-ganache": "yarn buidler test --network ganache",
"test-ethermint": "yarn buidler test --network ethermint"
},
"devDependencies": {
"@aragon/contract-helpers-test": "^0.1.0",
"@nomiclabs/buidler": "^1.4.3",
"@nomiclabs/buidler-ganache": "^1.3.3",
"@nomiclabs/buidler-truffle5": "^1.3.4",
"@nomiclabs/buidler-web3": "^1.3.4",
"chai": "^4.2.0",
"web3": "^1.2.11"
}
}

View File

@ -0,0 +1,74 @@
const { assertRevert } = require('@aragon/contract-helpers-test/src/asserts')
// Mocks
const LifecycleMock = artifacts.require('LifecycleMock')
const ERRORS = {
INIT_ALREADY_INITIALIZED: 'INIT_ALREADY_INITIALIZED',
}
contract('Lifecycle', () => {
let lifecycle
beforeEach(async () => {
lifecycle = await LifecycleMock.new()
})
it('is not initialized', async () => {
assert.isFalse(await lifecycle.hasInitialized(), 'should not be initialized')
})
it('is not petrified', async () => {
assert.isFalse(await lifecycle.isPetrified(), 'should not be petrified')
})
context('> Initialized', () => {
beforeEach(async () => {
await lifecycle.initializeMock()
})
it('is initialized', async () => {
assert.isTrue(await lifecycle.hasInitialized(), 'should be initialized')
})
it('is not petrified', async () => {
assert.isFalse(await lifecycle.isPetrified(), 'should not be petrified')
})
it('has correct initialization block', async () => {
assert.equal(await lifecycle.getInitializationBlock(), await web3.eth.getBlockNumber(), 'initialization block should be correct')
})
it('cannot be re-initialized', async () => {
await assertRevert(lifecycle.initializeMock()/*, ERRORS.INIT_ALREADY_INITIALIZED*/)
})
it('cannot be petrified', async () => {
await assertRevert(lifecycle.petrifyMock()/*, ERRORS.INIT_ALREADY_INITIALIZED*/)
})
})
context('> Petrified', () => {
beforeEach(async () => {
await lifecycle.petrifyMock()
})
it('is not initialized', async () => {
assert.isFalse(await lifecycle.hasInitialized(), 'should not be initialized')
})
it('is petrified', async () => {
assert.isTrue(await lifecycle.isPetrified(), 'should be petrified')
})
it('cannot be petrified again', async () => {
await assertRevert(lifecycle.petrifyMock()/*, ERRORS.INIT_ALREADY_INITIALIZED*/)
})
it('has initialization block in the future', async () => {
const petrifiedBlock = await lifecycle.getInitializationBlock()
const blockNumber = await web3.eth.getBlockNumber()
assert.isTrue(petrifiedBlock.gt(blockNumber), 'petrified block should be in the future')
})
})
})

View File

@ -0,0 +1,59 @@
/*
* SPDX-License-Identifier: MIT
*/
pragma solidity ^0.4.24;
import "./TimeHelpers.sol";
import "./UnstructuredStorage.sol";
contract Initializable is TimeHelpers {
using UnstructuredStorage for bytes32;
// keccak256("aragonOS.initializable.initializationBlock")
bytes32 internal constant INITIALIZATION_BLOCK_POSITION = 0xebb05b386a8d34882b8711d156f463690983dc47815980fb82aeeff1aa43579e;
string private constant ERROR_ALREADY_INITIALIZED = "INIT_ALREADY_INITIALIZED";
string private constant ERROR_NOT_INITIALIZED = "INIT_NOT_INITIALIZED";
modifier onlyInit {
require(getInitializationBlock() == 0, ERROR_ALREADY_INITIALIZED);
_;
}
modifier isInitialized {
require(hasInitialized(), ERROR_NOT_INITIALIZED);
_;
}
/**
* @return Block number in which the contract was initialized
*/
function getInitializationBlock() public view returns (uint256) {
return INITIALIZATION_BLOCK_POSITION.getStorageUint256();
}
/**
* @return Whether the contract has been initialized by the time of the current block
*/
function hasInitialized() public view returns (bool) {
uint256 initializationBlock = getInitializationBlock();
return initializationBlock != 0 && getBlockNumber() >= initializationBlock;
}
/**
* @dev Function to be called by top level contract after initialization has finished.
*/
function initialized() internal onlyInit {
INITIALIZATION_BLOCK_POSITION.setStorageUint256(getBlockNumber());
}
/**
* @dev Function to be called by top level contract after initialization to enable the contract
* at a future block number rather than immediately.
*/
function initializedAt(uint256 _blockNumber) internal onlyInit {
INITIALIZATION_BLOCK_POSITION.setStorageUint256(_blockNumber);
}
}

View File

@ -0,0 +1,21 @@
pragma solidity ^0.4.24;
import "./Initializable.sol";
contract Petrifiable is Initializable {
// Use block UINT256_MAX (which should be never) as the initializable date
uint256 internal constant PETRIFIED_BLOCK = uint256(-1);
function isPetrified() public view returns (bool) {
return getInitializationBlock() == PETRIFIED_BLOCK;
}
/**
* @dev Function to be called by top level contract to prevent being initialized.
* Useful for freezing base contracts when they're used behind proxies.
*/
function petrify() internal onlyInit {
initializedAt(PETRIFIED_BLOCK);
}
}

View File

@ -0,0 +1,44 @@
pragma solidity ^0.4.24;
import "./Uint256Helpers.sol";
contract TimeHelpers {
using Uint256Helpers for uint256;
/**
* @dev Returns the current block number.
* Using a function rather than `block.number` allows us to easily mock the block number in
* tests.
*/
function getBlockNumber() internal view returns (uint256) {
return block.number;
}
/**
* @dev Returns the current block number, converted to uint64.
* Using a function rather than `block.number` allows us to easily mock the block number in
* tests.
*/
function getBlockNumber64() internal view returns (uint64) {
return getBlockNumber().toUint64();
}
/**
* @dev Returns the current timestamp.
* Using a function rather than `block.timestamp` allows us to easily mock it in
* tests.
*/
function getTimestamp() internal view returns (uint256) {
return block.timestamp; // solium-disable-line security/no-block-members
}
/**
* @dev Returns the current timestamp, converted to uint64.
* Using a function rather than `block.timestamp` allows us to easily mock it in
* tests.
*/
function getTimestamp64() internal view returns (uint64) {
return getTimestamp().toUint64();
}
}

View File

@ -0,0 +1,13 @@
pragma solidity ^0.4.24;
library Uint256Helpers {
uint256 private constant MAX_UINT64 = uint64(-1);
string private constant ERROR_NUMBER_TOO_BIG = "UINT64_NUMBER_TOO_BIG";
function toUint64(uint256 a) internal pure returns (uint64) {
require(a <= MAX_UINT64, ERROR_NUMBER_TOO_BIG);
return uint64(a);
}
}

View File

@ -0,0 +1,36 @@
pragma solidity ^0.4.24;
library UnstructuredStorage {
function getStorageBool(bytes32 position) internal view returns (bool data) {
assembly { data := sload(position) }
}
function getStorageAddress(bytes32 position) internal view returns (address data) {
assembly { data := sload(position) }
}
function getStorageBytes32(bytes32 position) internal view returns (bytes32 data) {
assembly { data := sload(position) }
}
function getStorageUint256(bytes32 position) internal view returns (uint256 data) {
assembly { data := sload(position) }
}
function setStorageBool(bytes32 position, bool data) internal {
assembly { sstore(position, data) }
}
function setStorageAddress(bytes32 position, address data) internal {
assembly { sstore(position, data) }
}
function setStorageBytes32(bytes32 position, bytes32 data) internal {
assembly { sstore(position, data) }
}
function setStorageUint256(bytes32 position, uint256 data) internal {
assembly { sstore(position, data) }
}
}

View File

@ -0,0 +1,15 @@
pragma solidity 0.4.24;
import "../Initializable.sol";
import "../Petrifiable.sol";
contract LifecycleMock is Initializable, Petrifiable {
function initializeMock() public {
initialized();
}
function petrifyMock() public {
petrify();
}
}

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.8.0;
contract Migrations {
address public owner = msg.sender;
uint public last_completed_migration;
modifier restricted() {
require(
msg.sender == owner,
"This function is restricted to the contract's owner"
);
_;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
}

View File

@ -0,0 +1,5 @@
const Migrations = artifacts.require("Migrations");
module.exports = function (deployer) {
deployer.deploy(Migrations);
};

View File

@ -0,0 +1,16 @@
{
"name": "initializable",
"version": "1.0.0",
"author": "Aragon Association <contact@aragon.org>",
"license": "GPL-3.0-or-later",
"scripts": {
"test-ganache": "yarn truffle test",
"test-ethermint": "yarn truffle test --network ethermint"
},
"devDependencies": {
"@aragon/contract-helpers-test": "^0.1.0",
"chai": "^4.2.0",
"truffle": "^5.1.42",
"web3": "^1.2.11"
}
}

View File

@ -0,0 +1,74 @@
const { assertRevert } = require('@aragon/contract-helpers-test/src/asserts')
// Mocks
const LifecycleMock = artifacts.require('LifecycleMock')
const ERRORS = {
INIT_ALREADY_INITIALIZED: 'INIT_ALREADY_INITIALIZED',
}
contract('Lifecycle', () => {
let lifecycle
beforeEach(async () => {
lifecycle = await LifecycleMock.new()
})
it('is not initialized', async () => {
assert.isFalse(await lifecycle.hasInitialized(), 'should not be initialized')
})
it('is not petrified', async () => {
assert.isFalse(await lifecycle.isPetrified(), 'should not be petrified')
})
context('> Initialized', () => {
beforeEach(async () => {
await lifecycle.initializeMock()
})
it('is initialized', async () => {
assert.isTrue(await lifecycle.hasInitialized(), 'should be initialized')
})
it('is not petrified', async () => {
assert.isFalse(await lifecycle.isPetrified(), 'should not be petrified')
})
it('has correct initialization block', async () => {
assert.equal(await lifecycle.getInitializationBlock(), await web3.eth.getBlockNumber(), 'initialization block should be correct')
})
it('cannot be re-initialized', async () => {
await assertRevert(lifecycle.initializeMock()/*, ERRORS.INIT_ALREADY_INITIALIZED*/)
})
it('cannot be petrified', async () => {
await assertRevert(lifecycle.petrifyMock()/*, ERRORS.INIT_ALREADY_INITIALIZED*/)
})
})
context('> Petrified', () => {
beforeEach(async () => {
await lifecycle.petrifyMock()
})
it('is not initialized', async () => {
assert.isFalse(await lifecycle.hasInitialized(), 'should not be initialized')
})
it('is petrified', async () => {
assert.isTrue(await lifecycle.isPetrified(), 'should be petrified')
})
it('cannot be petrified again', async () => {
await assertRevert(lifecycle.petrifyMock()/*, ERRORS.INIT_ALREADY_INITIALIZED*/)
})
it('has initialization block in the future', async () => {
const petrifiedBlock = await lifecycle.getInitializationBlock()
const blockNumber = await web3.eth.getBlockNumber()
assert.isTrue(petrifiedBlock.gt(blockNumber), 'petrified block should be in the future')
})
})
})

View File

@ -0,0 +1,23 @@
module.exports = {
networks: {
// Development network is just left as truffle's default settings
ethermint: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
gas: 5000000, // Gas sent with each transaction
gasPrice: 1000000000, // 1 gwei (in wei)
},
},
compilers: {
solc: {
version: "0.4.24",
settings: {
optimizer: {
enabled: true,
runs: 10000,
},
},
},
},
}

View File

@ -0,0 +1,31 @@
pragma solidity 0.4.24;
import "./IsContract.sol";
import "./ERCProxy.sol";
contract DelegateProxy is ERCProxy, IsContract {
uint256 internal constant FWD_GAS_LIMIT = 10000;
/**
* @dev Performs a delegatecall and returns whatever the delegatecall returned (entire context execution will return!)
* @param _dst Destination address to perform the delegatecall
* @param _calldata Calldata for the delegatecall
*/
function delegatedFwd(address _dst, bytes _calldata) internal {
require(isContract(_dst));
uint256 fwdGasLimit = FWD_GAS_LIMIT;
assembly {
let result := delegatecall(sub(gas, fwdGasLimit), _dst, add(_calldata, 0x20), mload(_calldata), 0, 0)
let size := returndatasize
let ptr := mload(0x40)
returndatacopy(ptr, 0, size)
// revert instead of invalid() bc if the underlying call failed with invalid() it already wasted gas.
// if the call returned error data, forward it
switch result case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}

View File

@ -0,0 +1,44 @@
pragma solidity 0.4.24;
import "./DelegateProxy.sol";
import "./DepositableStorage.sol";
contract DepositableDelegateProxy is DepositableStorage, DelegateProxy {
event ProxyDeposit(address sender, uint256 value);
function () external payable {
uint256 forwardGasThreshold = FWD_GAS_LIMIT;
bytes32 isDepositablePosition = DEPOSITABLE_POSITION;
// Optimized assembly implementation to prevent EIP-1884 from breaking deposits, reference code in Solidity:
// https://github.com/aragon/aragonOS/blob/v4.2.1/contracts/common/DepositableDelegateProxy.sol#L10-L20
assembly {
// Continue only if the gas left is lower than the threshold for forwarding to the implementation code,
// otherwise continue outside of the assembly block.
if lt(gas, forwardGasThreshold) {
// Only accept the deposit and emit an event if all of the following are true:
// the proxy accepts deposits (isDepositable), msg.data.length == 0, and msg.value > 0
if and(and(sload(isDepositablePosition), iszero(calldatasize)), gt(callvalue, 0)) {
// Equivalent Solidity code for emitting the event:
// emit ProxyDeposit(msg.sender, msg.value);
let logData := mload(0x40) // free memory pointer
mstore(logData, caller) // add 'msg.sender' to the log data (first event param)
mstore(add(logData, 0x20), callvalue) // add 'msg.value' to the log data (second event param)
// Emit an event with one topic to identify the event: keccak256('ProxyDeposit(address,uint256)') = 0x15ee...dee1
log1(logData, 0x40, 0x15eeaa57c7bd188c1388020bcadc2c436ec60d647d36ef5b9eb3c742217ddee1)
stop() // Stop. Exits execution context
}
// If any of above checks failed, revert the execution (if ETH was sent, it is returned to the sender)
revert(0, 0)
}
}
address target = implementation();
delegatedFwd(target, msg.data);
}
}

View File

@ -0,0 +1,19 @@
pragma solidity 0.4.24;
import "./UnstructuredStorage.sol";
contract DepositableStorage {
using UnstructuredStorage for bytes32;
// keccak256("aragonOS.depositableStorage.depositable")
bytes32 internal constant DEPOSITABLE_POSITION = 0x665fd576fbbe6f247aff98f5c94a561e3f71ec2d3c988d56f12d342396c50cea;
function isDepositable() public view returns (bool) {
return DEPOSITABLE_POSITION.getStorageBool();
}
function setDepositable(bool _depositable) internal {
DEPOSITABLE_POSITION.setStorageBool(_depositable);
}
}

View File

@ -0,0 +1,10 @@
pragma solidity ^0.4.24;
contract ERCProxy {
uint256 internal constant FORWARDING = 1;
uint256 internal constant UPGRADEABLE = 2;
function proxyType() public pure returns (uint256 proxyTypeId);
function implementation() public view returns (address codeAddr);
}

View File

@ -0,0 +1,21 @@
pragma solidity ^0.4.24;
contract IsContract {
/*
* NOTE: this should NEVER be used for authentication
* (see pitfalls: https://github.com/fergarrui/ethereum-security/tree/master/contracts/extcodesize).
*
* This is only intended to be used as a sanity check that an address is actually a contract,
* RATHER THAN an address not being a contract.
*/
function isContract(address _target) internal view returns (bool) {
if (_target == address(0)) {
return false;
}
uint256 size;
assembly { size := extcodesize(_target) }
return size > 0;
}
}

View File

@ -0,0 +1,36 @@
pragma solidity ^0.4.24;
library UnstructuredStorage {
function getStorageBool(bytes32 position) internal view returns (bool data) {
assembly { data := sload(position) }
}
function getStorageAddress(bytes32 position) internal view returns (address data) {
assembly { data := sload(position) }
}
function getStorageBytes32(bytes32 position) internal view returns (bytes32 data) {
assembly { data := sload(position) }
}
function getStorageUint256(bytes32 position) internal view returns (uint256 data) {
assembly { data := sload(position) }
}
function setStorageBool(bytes32 position, bool data) internal {
assembly { sstore(position, data) }
}
function setStorageAddress(bytes32 position, address data) internal {
assembly { sstore(position, data) }
}
function setStorageBytes32(bytes32 position, bytes32 data) internal {
assembly { sstore(position, data) }
}
function setStorageUint256(bytes32 position, uint256 data) internal {
assembly { sstore(position, data) }
}
}

View File

@ -0,0 +1,24 @@
pragma solidity 0.4.24;
import "../DepositableDelegateProxy.sol";
contract DepositableDelegateProxyMock is DepositableDelegateProxy {
address private implementationMock;
function enableDepositsOnMock() external {
setDepositable(true);
}
function setImplementationOnMock(address _implementationMock) external {
implementationMock = _implementationMock;
}
function implementation() public view returns (address) {
return implementationMock;
}
function proxyType() public pure returns (uint256 proxyTypeId) {
return UPGRADEABLE;
}
}

View File

@ -0,0 +1,8 @@
pragma solidity 0.4.24;
contract EthSender {
function sendEth(address to) external payable {
to.transfer(msg.value);
}
}

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.8.0;
contract Migrations {
address public owner = msg.sender;
uint public last_completed_migration;
modifier restricted() {
require(
msg.sender == owner,
"This function is restricted to the contract's owner"
);
_;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
}

View File

@ -0,0 +1,17 @@
pragma solidity 0.4.24;
contract ProxyTargetWithoutFallback {
event Pong();
function ping() external {
emit Pong();
}
}
contract ProxyTargetWithFallback is ProxyTargetWithoutFallback {
event ReceivedEth();
function () external payable {
emit ReceivedEth();
}
}

View File

@ -0,0 +1,5 @@
const Migrations = artifacts.require("Migrations");
module.exports = function (deployer) {
deployer.deploy(Migrations);
};

View File

@ -0,0 +1,16 @@
{
"name": "proxy",
"version": "1.0.0",
"author": "Aragon Association <contact@aragon.org>",
"license": "GPL-3.0-or-later",
"scripts": {
"test-ganache": "yarn truffle test",
"test-ethermint": "yarn truffle test --network ethermint"
},
"devDependencies": {
"@aragon/contract-helpers-test": "^0.1.0",
"chai": "^4.2.0",
"truffle": "^5.1.42",
"web3": "^1.2.11"
}
}

View File

@ -0,0 +1,155 @@
const { bn } = require('@aragon/contract-helpers-test')
const { assertAmountOfEvents, assertEvent, assertRevert, assertOutOfGas, assertBn } = require('@aragon/contract-helpers-test/src/asserts')
// Mocks
const DepositableDelegateProxyMock = artifacts.require('DepositableDelegateProxyMock')
const EthSender = artifacts.require('EthSender')
const ProxyTargetWithoutFallback = artifacts.require('ProxyTargetWithoutFallback')
const ProxyTargetWithFallback = artifacts.require('ProxyTargetWithFallback')
const TX_BASE_GAS = 21000
const SEND_ETH_GAS = TX_BASE_GAS + 9999 // <10k gas is the threshold for depositing
const PROXY_FORWARD_GAS = TX_BASE_GAS + 2e6 // high gas amount to ensure that the proxy forwards the call
const FALLBACK_SETUP_GAS = 100 // rough estimation of how much gas it spends before executing the fallback code
const SOLIDITY_TRANSFER_GAS = 2300
contract('DepositableDelegateProxy', ([ sender ]) => {
let ethSender, proxy, target, proxyTargetWithoutFallbackBase, proxyTargetWithFallbackBase
// Initial setup
before(async () => {
ethSender = await EthSender.new()
proxyTargetWithoutFallbackBase = await ProxyTargetWithoutFallback.new()
proxyTargetWithFallbackBase = await ProxyTargetWithFallback.new()
})
beforeEach(async () => {
proxy = await DepositableDelegateProxyMock.new()
target = await ProxyTargetWithFallback.at(proxy.address)
})
const itForwardsToImplementationIfGasIsOverThreshold = () => {
context('when implementation address is set', () => {
const itSuccessfullyForwardsCall = () => {
it('forwards call with data', async () => {
const receipt = await target.ping({ gas: PROXY_FORWARD_GAS })
assertAmountOfEvents(receipt, 'Pong')
})
}
context('when implementation has a fallback', () => {
beforeEach(async () => {
await proxy.setImplementationOnMock(proxyTargetWithFallbackBase.address)
})
itSuccessfullyForwardsCall()
it('can receive ETH [@skip-on-coverage]', async () => {
const receipt = await target.sendTransaction({ value: 1, gas: SEND_ETH_GAS + FALLBACK_SETUP_GAS })
assertAmountOfEvents(receipt, 'ReceivedEth')
})
})
context('when implementation doesn\'t have a fallback', () => {
beforeEach(async () => {
await proxy.setImplementationOnMock(proxyTargetWithoutFallbackBase.address)
})
itSuccessfullyForwardsCall()
it('reverts when sending ETH', async () => {
await assertRevert(target.sendTransaction({ value: 1, gas: PROXY_FORWARD_GAS }))
})
})
})
context('when implementation address is not set', () => {
it('reverts when a function is called', async () => {
await assertRevert(target.ping({ gas: PROXY_FORWARD_GAS }))
})
it('reverts when sending ETH', async () => {
await assertRevert(target.sendTransaction({ value: 1, gas: PROXY_FORWARD_GAS }))
})
})
}
const itRevertsOnInvalidDeposits = () => {
it('reverts when call has data', async () => {
await proxy.setImplementationOnMock(proxyTargetWithoutFallbackBase.address)
await assertRevert(target.ping({ gas: SEND_ETH_GAS }))
})
it('reverts when call sends 0 value', async () => {
await assertRevert(proxy.sendTransaction({ value: 0, gas: SEND_ETH_GAS }))
})
}
context('when proxy is set as depositable', () => {
beforeEach(async () => {
await proxy.enableDepositsOnMock()
})
context('when call gas is below the forwarding threshold', () => {
const value = bn(100)
const assertSendEthToProxy = async ({ value, gas, shouldOOG }) => {
const initialBalance = bn(await web3.eth.getBalance(proxy.address))
const sendEthAction = () => proxy.sendTransaction({ from: sender, gas, value })
if (shouldOOG) {
await assertOutOfGas(sendEthAction())
assertBn(bn(await web3.eth.getBalance(proxy.address)), initialBalance, 'Target balance should be the same as before')
} else {
const receipt = await sendEthAction()
assertBn(bn(await web3.eth.getBalance(proxy.address)), initialBalance.add(value), 'Target balance should be correct')
assertAmountOfEvents(receipt, 'ProxyDeposit', { decodeForAbi: DepositableDelegateProxyMock.abi })
assertEvent(receipt, 'ProxyDeposit', { decodeForAbi: DepositableDelegateProxyMock.abi, expectedArgs: { sender, value } })
return receipt
}
}
it('can receive ETH', async () => {
await assertSendEthToProxy({ value, gas: SEND_ETH_GAS })
})
it('cannot receive ETH if sent with a small amount of gas [@skip-on-coverage]', async () => {
const oogDecrease = 250
// deposit cannot be done with this amount of gas
const gas = TX_BASE_GAS + SOLIDITY_TRANSFER_GAS - oogDecrease
await assertSendEthToProxy({ shouldOOG: true, value, gas })
})
it('can receive ETH from contract [@skip-on-coverage]', async () => {
const receipt = await ethSender.sendEth(proxy.address, { value })
assertAmountOfEvents(receipt, 'ProxyDeposit', { decodeForAbi: proxy.abi })
assertEvent(receipt, 'ProxyDeposit', { decodeForAbi: proxy.abi, expectedArgs: { sender: ethSender.address, value } })
})
itRevertsOnInvalidDeposits()
})
context('when call gas is over forwarding threshold', () => {
itForwardsToImplementationIfGasIsOverThreshold()
})
})
context('when proxy is not set as depositable', () => {
context('when call gas is below the forwarding threshold', () => {
it('reverts when depositing ETH', async () => {
await assertRevert(proxy.sendTransaction({ value: 1, gas: SEND_ETH_GAS }))
})
itRevertsOnInvalidDeposits()
})
context('when call gas is over forwarding threshold', () => {
itForwardsToImplementationIfGasIsOverThreshold()
})
})
})

View File

@ -0,0 +1,23 @@
module.exports = {
networks: {
// Development network is just left as truffle's default settings
ethermint: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
gas: 5000000, // Gas sent with each transaction
gasPrice: 1000000000, // 1 gwei (in wei)
},
},
compilers: {
solc: {
version: "0.4.24",
settings: {
optimizer: {
enabled: true,
runs: 10000,
},
},
},
},
}

View File

@ -0,0 +1,28 @@
name: contracts
on:
push:
branches: master
pull_request:
branches: '*'
jobs:
CI:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install node
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install
run: yarn
- name: Lint
run: yarn lint
- name: Test
run: yarn test
- name: coverage
continue-on-error: true
run: yarn coverage
env:
CI: true

View File

@ -0,0 +1,648 @@
pragma solidity 0.5.17;
import "./lib/os/SafeMath.sol";
import "./lib/os/SafeERC20.sol";
import "./lib/os/IsContract.sol";
import "./lib/os/Autopetrified.sol";
import "./lib/Checkpointing.sol";
import "./standards/ERC900.sol";
import "./locking/IStakingLocking.sol";
import "./locking/ILockManager.sol";
contract Staking is Autopetrified, ERC900, IStakingLocking, IsContract {
using SafeMath for uint256;
using Checkpointing for Checkpointing.History;
using SafeERC20 for ERC20;
uint256 private constant MAX_UINT64 = uint256(uint64(-1));
string private constant ERROR_TOKEN_NOT_CONTRACT = "STAKING_TOKEN_NOT_CONTRACT";
string private constant ERROR_AMOUNT_ZERO = "STAKING_AMOUNT_ZERO";
string private constant ERROR_TOKEN_TRANSFER = "STAKING_TOKEN_TRANSFER_FAIL";
string private constant ERROR_TOKEN_DEPOSIT = "STAKING_TOKEN_DEPOSIT_FAIL";
string private constant ERROR_TOKEN_NOT_SENDER = "STAKING_TOKEN_NOT_SENDER";
string private constant ERROR_WRONG_TOKEN = "STAKING_WRONG_TOKEN";
string private constant ERROR_NOT_ENOUGH_BALANCE = "STAKING_NOT_ENOUGH_BALANCE";
string private constant ERROR_NOT_ENOUGH_ALLOWANCE = "STAKING_NOT_ENOUGH_ALLOWANCE";
string private constant ERROR_SENDER_NOT_ALLOWED = "STAKING_SENDER_NOT_ALLOWED";
string private constant ERROR_ALLOWANCE_ZERO = "STAKING_ALLOWANCE_ZERO";
string private constant ERROR_LOCK_ALREADY_EXISTS = "STAKING_LOCK_ALREADY_EXISTS";
string private constant ERROR_LOCK_DOES_NOT_EXIST = "STAKING_LOCK_DOES_NOT_EXIST";
string private constant ERROR_NOT_ENOUGH_LOCK = "STAKING_NOT_ENOUGH_LOCK";
string private constant ERROR_CANNOT_UNLOCK = "STAKING_CANNOT_UNLOCK";
string private constant ERROR_CANNOT_CHANGE_ALLOWANCE = "STAKING_CANNOT_CHANGE_ALLOWANCE";
string private constant ERROR_LOCKMANAGER_CALL_FAIL = "STAKING_LOCKMANAGER_CALL_FAIL";
string private constant ERROR_BLOCKNUMBER_TOO_BIG = "STAKING_BLOCKNUMBER_TOO_BIG";
struct Lock {
uint256 amount;
uint256 allowance; // must be greater than zero to consider the lock active, and always greater than or equal to amount
}
struct Account {
mapping (address => Lock) locks; // from manager to lock
uint256 totalLocked;
Checkpointing.History stakedHistory;
}
ERC20 internal stakingToken;
mapping (address => Account) internal accounts;
Checkpointing.History internal totalStakedHistory;
/**
* @notice Initialize Staking app with token `_stakingToken`
* @param _stakingToken ERC20 token used for staking
*/
function initialize(ERC20 _stakingToken) external {
require(isContract(address(_stakingToken)), ERROR_TOKEN_NOT_CONTRACT);
initialized();
stakingToken = _stakingToken;
}
/**
* @notice Stakes `@tokenAmount(self.token(): address, _amount)`, transferring them from `msg.sender`
* @param _amount Number of tokens staked
* @param _data Used in Staked event, to add signalling information in more complex staking applications
*/
function stake(uint256 _amount, bytes calldata _data) external isInitialized {
_stakeFor(msg.sender, msg.sender, _amount, _data);
}
/**
* @notice Stakes `@tokenAmount(self.token(): address, _amount)`, transferring them from `msg.sender`, and assigns them to `_user`
* @param _user The receiving accounts for the tokens staked
* @param _amount Number of tokens staked
* @param _data Used in Staked event, to add signalling information in more complex staking applications
*/
function stakeFor(address _user, uint256 _amount, bytes calldata _data) external isInitialized {
_stakeFor(msg.sender, _user, _amount, _data);
}
/**
* @notice Unstakes `@tokenAmount(self.token(): address, _amount)`, returning them to the user
* @param _amount Number of tokens to unstake
* @param _data Used in Unstaked event, to add signalling information in more complex staking applications
*/
function unstake(uint256 _amount, bytes calldata _data) external isInitialized {
// unstaking 0 tokens is not allowed
require(_amount > 0, ERROR_AMOUNT_ZERO);
_unstake(msg.sender, _amount, _data);
}
/**
* @notice Allow `_lockManager` to lock up to `@tokenAmount(self.token(): address, _allowance)` of `msg.sender`
* It creates a new lock, so the lock for this manager cannot exist before.
* @param _lockManager The manager entity for this particular lock
* @param _allowance Amount of tokens that the manager can lock
* @param _data Data to parametrize logic for the lock to be enforced by the manager
*/
function allowManager(address _lockManager, uint256 _allowance, bytes calldata _data) external isInitialized {
_allowManager(_lockManager, _allowance, _data);
}
/**
* @notice Lock `@tokenAmount(self.token(): address, _amount)` and assign `_lockManager` as manager with `@tokenAmount(self.token(): address, _allowance)` allowance and `_data` as data, so they can not be unstaked
* @param _amount The amount of tokens to be locked
* @param _lockManager The manager entity for this particular lock. This entity will have full control over the lock, in particular will be able to unlock it
* @param _allowance Amount of tokens that the manager can lock
* @param _data Data to parametrize logic for the lock to be enforced by the manager
*/
function allowManagerAndLock(uint256 _amount, address _lockManager, uint256 _allowance, bytes calldata _data) external isInitialized {
_allowManager(_lockManager, _allowance, _data);
_lockUnsafe(msg.sender, _lockManager, _amount);
}
/**
* @notice Transfer `@tokenAmount(self.token(): address, _amount)` to `_to`s staked balance
* @param _to Recipient of the tokens
* @param _amount Number of tokens to be transferred
*/
function transfer(address _to, uint256 _amount) external isInitialized {
_transfer(msg.sender, _to, _amount);
}
/**
* @notice Transfer `@tokenAmount(self.token(): address, _amount)` to `_to`s external balance (i.e. unstaked)
* @param _to Recipient of the tokens
* @param _amount Number of tokens to be transferred
*/
function transferAndUnstake(address _to, uint256 _amount) external isInitialized {
_transfer(msg.sender, _to, _amount);
_unstake(_to, _amount, new bytes(0));
}
/**
* @notice Transfer `@tokenAmount(self.token(): address, _amount)` from `_from`'s lock by `msg.sender` to `_to`
* @param _from Owner of locked tokens
* @param _to Recipient of the tokens
* @param _amount Number of tokens to be transferred
*/
function slash(
address _from,
address _to,
uint256 _amount
)
external
isInitialized
{
_unlockUnsafe(_from, msg.sender, _amount);
_transfer(_from, _to, _amount);
}
/**
* @notice Transfer `@tokenAmount(self.token(): address, _amount)` from `_from`'s lock by `msg.sender` to `_to` (unstaked)
* @param _from Owner of locked tokens
* @param _to Recipient of the tokens
* @param _amount Number of tokens to be transferred
*/
function slashAndUnstake(
address _from,
address _to,
uint256 _amount
)
external
isInitialized
{
_unlockUnsafe(_from, msg.sender, _amount);
_transfer(_from, _to, _amount);
_unstake(_to, _amount, new bytes(0));
}
/**
* @notice Transfer `@tokenAmount(self.token(): address, _slashAmount)` from `_from`'s lock by `msg.sender` to `_to`, and decrease `@tokenAmount(self.token(): address, _unlockAmount)` from that lock
* @param _from Owner of locked tokens
* @param _to Recipient of the tokens
* @param _unlockAmount Number of tokens to be unlocked
* @param _slashAmount Number of tokens to be transferred
*/
function slashAndUnlock(
address _from,
address _to,
uint256 _unlockAmount,
uint256 _slashAmount
)
external
isInitialized
{
// No need to check that _slashAmount is positive, as _transfer will fail
// No need to check that have enough locked funds, as _unlockUnsafe will fail
require(_unlockAmount > 0, ERROR_AMOUNT_ZERO);
_unlockUnsafe(_from, msg.sender, _unlockAmount.add(_slashAmount));
_transfer(_from, _to, _slashAmount);
}
/**
* @notice Increase allowance by `@tokenAmount(self.token(): address, _allowance)` of lock manager `_lockManager` for user `msg.sender`
* @param _lockManager The manager entity for this particular lock
* @param _allowance Amount of allowed tokens increase
*/
function increaseLockAllowance(address _lockManager, uint256 _allowance) external isInitialized {
Lock storage lock_ = accounts[msg.sender].locks[_lockManager];
require(lock_.allowance > 0, ERROR_LOCK_DOES_NOT_EXIST);
_increaseLockAllowance(_lockManager, lock_, _allowance);
}
/**
* @notice Decrease allowance by `@tokenAmount(self.token(): address, _allowance)` of lock manager `_lockManager` for user `_user`
* @param _user Owner of locked tokens
* @param _lockManager The manager entity for this particular lock
* @param _allowance Amount of allowed tokens decrease
*/
function decreaseLockAllowance(address _user, address _lockManager, uint256 _allowance) external isInitialized {
// only owner and manager can decrease allowance
require(msg.sender == _user || msg.sender == _lockManager, ERROR_CANNOT_CHANGE_ALLOWANCE);
require(_allowance > 0, ERROR_AMOUNT_ZERO);
Lock storage lock_ = accounts[_user].locks[_lockManager];
uint256 newAllowance = lock_.allowance.sub(_allowance);
require(newAllowance >= lock_.amount, ERROR_NOT_ENOUGH_ALLOWANCE);
// unlockAndRemoveManager must be used for this:
require(newAllowance > 0, ERROR_ALLOWANCE_ZERO);
lock_.allowance = newAllowance;
emit LockAllowanceChanged(_user, _lockManager, _allowance, false);
}
/**
* @notice Increase locked amount by `@tokenAmount(self.token(): address, _amount)` for user `_user` by lock manager `_lockManager`
* @param _user Owner of locked tokens
* @param _lockManager The manager entity for this particular lock
* @param _amount Amount of locked tokens increase
*/
function lock(address _user, address _lockManager, uint256 _amount) external isInitialized {
// we are locking funds from owner account, so only owner or manager are allowed
require(msg.sender == _user || msg.sender == _lockManager, ERROR_SENDER_NOT_ALLOWED);
_lockUnsafe(_user, _lockManager, _amount);
}
/**
* @notice Decrease locked amount by `@tokenAmount(self.token(): address, _amount)` for user `_user` by lock manager `_lockManager`
* @param _user Owner of locked tokens
* @param _lockManager The manager entity for this particular lock
* @param _amount Amount of locked tokens decrease
*/
function unlock(address _user, address _lockManager, uint256 _amount) external isInitialized {
require(_amount > 0, ERROR_AMOUNT_ZERO);
// only manager and owner (if manager allows) can unlock
require(_canUnlockUnsafe(msg.sender, _user, _lockManager, _amount), ERROR_CANNOT_UNLOCK);
_unlockUnsafe(_user, _lockManager, _amount);
}
/**
* @notice Unlock `_user`'s lock by `_lockManager` so locked tokens can be unstaked again
* @param _user Owner of locked tokens
* @param _lockManager Manager of the lock for the given account
*/
function unlockAndRemoveManager(address _user, address _lockManager) external isInitialized {
// only manager and owner (if manager allows) can unlock
require(_canUnlockUnsafe(msg.sender, _user, _lockManager, 0), ERROR_CANNOT_UNLOCK);
Account storage account = accounts[_user];
Lock storage lock_ = account.locks[_lockManager];
uint256 amount = lock_.amount;
// update total
account.totalLocked = account.totalLocked.sub(amount);
emit LockAmountChanged(_user, _lockManager, amount, false);
emit LockManagerRemoved(_user, _lockManager);
delete account.locks[_lockManager];
}
/**
* @notice Change the manager of `_user`'s lock from `msg.sender` to `_newLockManager`
* @param _user Owner of lock
* @param _newLockManager New lock manager
*/
function setLockManager(address _user, address _newLockManager) external isInitialized {
Lock storage lock_ = accounts[_user].locks[msg.sender];
require(lock_.allowance > 0, ERROR_LOCK_DOES_NOT_EXIST);
accounts[_user].locks[_newLockManager] = lock_;
delete accounts[_user].locks[msg.sender];
emit LockManagerTransferred(_user, msg.sender, _newLockManager);
}
/**
* @dev MiniMeToken ApproveAndCallFallBack compliance
* @param _from Account approving tokens
* @param _amount Amount of `_token` tokens being approved
* @param _token MiniMeToken that is being approved and that the call comes from
* @param _data Used in Staked event, to add signalling information in more complex staking applications
*/
function receiveApproval(address _from, uint256 _amount, address _token, bytes calldata _data) external isInitialized {
require(_token == msg.sender, ERROR_TOKEN_NOT_SENDER);
require(_token == address(stakingToken), ERROR_WRONG_TOKEN);
_stakeFor(_from, _from, _amount, _data);
}
/**
* @notice Check whether it supports history of stakes
* @return Always true
*/
function supportsHistory() external pure returns (bool) {
return true;
}
/**
* @notice Get the token used by the contract for staking and locking
* @return The token used by the contract for staking and locking
*/
function token() external view isInitialized returns (address) {
return address(stakingToken);
}
/**
* @notice Get last time `_user` modified its staked balance
* @param _user Account requesting for
* @return Last block number when account's balance was modified
*/
function lastStakedFor(address _user) external view isInitialized returns (uint256) {
return accounts[_user].stakedHistory.lastUpdate();
}
/**
* @notice Get total amount of locked tokens for `_user`
* @param _user Owner of locks
* @return Total amount of locked tokens for the requested account
*/
function lockedBalanceOf(address _user) external view isInitialized returns (uint256) {
return _lockedBalanceOf(_user);
}
/**
* @notice Get details of `_user`'s lock by `_lockManager`
* @param _user Owner of lock
* @param _lockManager Manager of the lock for the given account
* @return Amount of locked tokens
* @return Amount of tokens that lock manager is allowed to lock
*/
function getLock(address _user, address _lockManager)
external
view
isInitialized
returns (
uint256 _amount,
uint256 _allowance
)
{
Lock storage lock_ = accounts[_user].locks[_lockManager];
_amount = lock_.amount;
_allowance = lock_.allowance;
}
/**
* @notice Get staked and locked balances of `_user`
* @param _user Account being requested
* @return Amount of staked tokens
* @return Amount of total locked tokens
*/
function getBalancesOf(address _user) external view isInitialized returns (uint256 staked, uint256 locked) {
staked = _totalStakedFor(_user);
locked = _lockedBalanceOf(_user);
}
/**
* @notice Get the amount of tokens staked by `_user`
* @param _user The owner of the tokens
* @return The amount of tokens staked by the given account
*/
function totalStakedFor(address _user) external view isInitialized returns (uint256) {
return _totalStakedFor(_user);
}
/**
* @notice Get the total amount of tokens staked by all users
* @return The total amount of tokens staked by all users
*/
function totalStaked() external view isInitialized returns (uint256) {
return _totalStaked();
}
/**
* @notice Get the total amount of tokens staked by `_user` at block number `_blockNumber`
* @param _user Account requesting for
* @param _blockNumber Block number at which we are requesting
* @return The amount of tokens staked by the account at the given block number
*/
function totalStakedForAt(address _user, uint256 _blockNumber) external view isInitialized returns (uint256) {
require(_blockNumber <= MAX_UINT64, ERROR_BLOCKNUMBER_TOO_BIG);
return accounts[_user].stakedHistory.get(uint64(_blockNumber));
}
/**
* @notice Get the total amount of tokens staked by all users at block number `_blockNumber`
* @param _blockNumber Block number at which we are requesting
* @return The amount of tokens staked at the given block number
*/
function totalStakedAt(uint256 _blockNumber) external view isInitialized returns (uint256) {
require(_blockNumber <= MAX_UINT64, ERROR_BLOCKNUMBER_TOO_BIG);
return totalStakedHistory.get(uint64(_blockNumber));
}
/**
* @notice Get the staked but unlocked amount of tokens by `_user`
* @param _user Owner of the staked but unlocked balance
* @return Amount of tokens staked but not locked by given account
*/
function unlockedBalanceOf(address _user) external view isInitialized returns (uint256) {
return _unlockedBalanceOf(_user);
}
/**
* @notice Check if `_sender` can unlock `_user`'s `@tokenAmount(self.token(): address, _amount)` locked by `_lockManager`
* @param _sender Account that would try to unlock tokens
* @param _user Owner of lock
* @param _lockManager Manager of the lock for the given owner
* @param _amount Amount of tokens to be potentially unlocked. If zero, it means the whole locked amount
* @return Whether given lock of given owner can be unlocked by given sender
*/
function canUnlock(address _sender, address _user, address _lockManager, uint256 _amount) external view isInitialized returns (bool) {
return _canUnlockUnsafe(_sender, _user, _lockManager, _amount);
}
function _stakeFor(address _from, address _user, uint256 _amount, bytes memory _data) internal {
// staking 0 tokens is invalid
require(_amount > 0, ERROR_AMOUNT_ZERO);
// checkpoint updated staking balance
uint256 newStake = _modifyStakeBalance(_user, _amount, true);
// checkpoint total supply
_modifyTotalStaked(_amount, true);
// pull tokens into Staking contract
require(stakingToken.safeTransferFrom(_from, address(this), _amount), ERROR_TOKEN_DEPOSIT);
emit Staked(_user, _amount, newStake, _data);
}
function _unstake(address _from, uint256 _amount, bytes memory _data) internal {
// checkpoint updated staking balance
uint256 newStake = _modifyStakeBalance(_from, _amount, false);
// checkpoint total supply
_modifyTotalStaked(_amount, false);
// transfer tokens
require(stakingToken.safeTransfer(_from, _amount), ERROR_TOKEN_TRANSFER);
emit Unstaked(_from, _amount, newStake, _data);
}
function _modifyStakeBalance(address _user, uint256 _by, bool _increase) internal returns (uint256) {
uint256 currentStake = _totalStakedFor(_user);
uint256 newStake;
if (_increase) {
newStake = currentStake.add(_by);
} else {
require(_by <= _unlockedBalanceOf(_user), ERROR_NOT_ENOUGH_BALANCE);
newStake = currentStake.sub(_by);
}
// add new value to account history
accounts[_user].stakedHistory.add(getBlockNumber64(), newStake);
return newStake;
}
function _modifyTotalStaked(uint256 _by, bool _increase) internal {
uint256 currentStake = _totalStaked();
uint256 newStake;
if (_increase) {
newStake = currentStake.add(_by);
} else {
newStake = currentStake.sub(_by);
}
// add new value to total history
totalStakedHistory.add(getBlockNumber64(), newStake);
}
function _allowManager(address _lockManager, uint256 _allowance, bytes memory _data) internal {
Lock storage lock_ = accounts[msg.sender].locks[_lockManager];
// check if lock exists
require(lock_.allowance == 0, ERROR_LOCK_ALREADY_EXISTS);
emit NewLockManager(msg.sender, _lockManager, _data);
_increaseLockAllowance(_lockManager, lock_, _allowance);
}
function _increaseLockAllowance(address _lockManager, Lock storage _lock, uint256 _allowance) internal {
require(_allowance > 0, ERROR_AMOUNT_ZERO);
_lock.allowance = _lock.allowance.add(_allowance);
emit LockAllowanceChanged(msg.sender, _lockManager, _allowance, true);
}
/**
* @dev Assumes that sender is either owner or lock manager
*/
function _lockUnsafe(address _user, address _lockManager, uint256 _amount) internal {
require(_amount > 0, ERROR_AMOUNT_ZERO);
// check enough unlocked tokens are available
require(_amount <= _unlockedBalanceOf(_user), ERROR_NOT_ENOUGH_BALANCE);
Account storage account = accounts[_user];
Lock storage lock_ = account.locks[_lockManager];
uint256 newAmount = lock_.amount.add(_amount);
// check allowance is enough, it also means that lock exists, as newAmount is greater than zero
require(newAmount <= lock_.allowance, ERROR_NOT_ENOUGH_ALLOWANCE);
lock_.amount = newAmount;
// update total
account.totalLocked = account.totalLocked.add(_amount);
emit LockAmountChanged(_user, _lockManager, _amount, true);
}
/**
* @dev Assumes `canUnlock` passes
*/
function _unlockUnsafe(address _user, address _lockManager, uint256 _amount) internal {
Account storage account = accounts[_user];
Lock storage lock_ = account.locks[_lockManager];
uint256 lockAmount = lock_.amount;
require(lockAmount >= _amount, ERROR_NOT_ENOUGH_LOCK);
// update lock amount
// No need for SafeMath: checked just above
lock_.amount = lockAmount - _amount;
// update total
account.totalLocked = account.totalLocked.sub(_amount);
emit LockAmountChanged(_user, _lockManager, _amount, false);
}
function _transfer(address _from, address _to, uint256 _amount) internal {
// transferring 0 staked tokens is invalid
require(_amount > 0, ERROR_AMOUNT_ZERO);
// update stakes
_modifyStakeBalance(_from, _amount, false);
_modifyStakeBalance(_to, _amount, true);
emit StakeTransferred(_from, _to, _amount);
}
/**
* @notice Get the amount of tokens staked by `_user`
* @param _user The owner of the tokens
* @return The amount of tokens staked by the given account
*/
function _totalStakedFor(address _user) internal view returns (uint256) {
// we assume it's not possible to stake in the future
return accounts[_user].stakedHistory.getLast();
}
/**
* @notice Get the total amount of tokens staked by all users
* @return The total amount of tokens staked by all users
*/
function _totalStaked() internal view returns (uint256) {
// we assume it's not possible to stake in the future
return totalStakedHistory.getLast();
}
/**
* @notice Get the staked but unlocked amount of tokens by `_user`
* @param _user Owner of the staked but unlocked balance
* @return Amount of tokens staked but not locked by given account
*/
function _unlockedBalanceOf(address _user) internal view returns (uint256) {
return _totalStakedFor(_user).sub(_lockedBalanceOf(_user));
}
function _lockedBalanceOf(address _user) internal view returns (uint256) {
return accounts[_user].totalLocked;
}
/**
* @notice Check if `_sender` can unlock `_user`'s `@tokenAmount(self.token(): address, _amount)` locked by `_lockManager`
* @dev If calling this from a state modifying function trying to unlock tokens, make sure first parameter is `msg.sender`
* @param _sender Account that would try to unlock tokens
* @param _user Owner of lock
* @param _lockManager Manager of the lock for the given owner
* @param _amount Amount of locked tokens to unlock. If zero, the full locked amount
* @return Whether given lock of given owner can be unlocked by given sender
*/
function _canUnlockUnsafe(address _sender, address _user, address _lockManager, uint256 _amount) internal view returns (bool) {
Lock storage lock_ = accounts[_user].locks[_lockManager];
require(lock_.allowance > 0, ERROR_LOCK_DOES_NOT_EXIST);
require(lock_.amount >= _amount, ERROR_NOT_ENOUGH_LOCK);
uint256 amount = _amount == 0 ? lock_.amount : _amount;
// If the sender is the lock manager, unlocking is allowed
if (_sender == _lockManager) {
return true;
}
// If the sender is neither the lock manager nor the owner, unlocking is not allowed
if (_sender != _user) {
return false;
}
// The sender must therefore be the owner of the tokens
// Allow unlocking if the amount of locked tokens has already been decreased to 0
if (amount == 0) {
return true;
}
// Otherwise, check whether the lock manager allows unlocking
return ILockManager(_lockManager).canUnlock(_user, amount);
}
function _toBytes4(bytes memory _data) internal pure returns (bytes4 result) {
if (_data.length < 4) {
return bytes4(0);
}
assembly { result := mload(add(_data, 0x20)) }
}
}

View File

@ -0,0 +1,44 @@
pragma solidity ^0.5.17;
import "./lib/os/ERC20.sol";
import "./Staking.sol";
import "./proxies/StakingProxy.sol";
contract StakingFactory {
Staking public baseImplementation;
mapping (address => address) internal instances;
event NewStaking(address indexed instance, address token);
constructor() public {
baseImplementation = new Staking();
}
function existsInstance(ERC20 token) external view returns (bool) {
return _getInstance(token) != address(0);
}
function getInstance(ERC20 token) external view returns (Staking) {
return Staking(_getInstance(token));
}
function getOrCreateInstance(ERC20 token) external returns (Staking) {
address instance = _getInstance(token);
return instance != address(0) ? Staking(instance) : _createInstance(token);
}
function _getInstance(ERC20 token) internal view returns (address) {
return instances[address(token)];
}
function _createInstance(ERC20 token) internal returns (Staking) {
StakingProxy instance = new StakingProxy(baseImplementation, token);
address tokenAddress = address(token);
address instanceAddress = address(instance);
instances[tokenAddress] = instanceAddress;
emit NewStaking(instanceAddress, tokenAddress);
return Staking(instanceAddress);
}
}

View File

@ -0,0 +1,155 @@
pragma solidity ^0.5.17;
/**
* @title Checkpointing - Library to handle a historic set of numeric values
*/
library Checkpointing {
uint256 private constant MAX_UINT192 = uint256(uint192(-1));
string private constant ERROR_VALUE_TOO_BIG = "CHECKPOINT_VALUE_TOO_BIG";
string private constant ERROR_CANNOT_ADD_PAST_VALUE = "CHECKPOINT_CANNOT_ADD_PAST_VALUE";
/**
* @dev To specify a value at a given point in time, we need to store two values:
* - `time`: unit-time value to denote the first time when a value was registered
* - `value`: a positive numeric value to registered at a given point in time
*
* Note that `time` does not need to refer necessarily to a timestamp value, any time unit could be used
* for it like block numbers, terms, etc.
*/
struct Checkpoint {
uint64 time;
uint192 value;
}
/**
* @dev A history simply denotes a list of checkpoints
*/
struct History {
Checkpoint[] history;
}
/**
* @dev Add a new value to a history for a given point in time. This function does not allow to add values previous
* to the latest registered value, if the value willing to add corresponds to the latest registered value, it
* will be updated.
* @param self Checkpoints history to be altered
* @param _time Point in time to register the given value
* @param _value Numeric value to be registered at the given point in time
*/
function add(History storage self, uint64 _time, uint256 _value) internal {
require(_value <= MAX_UINT192, ERROR_VALUE_TOO_BIG);
_add192(self, _time, uint192(_value));
}
/**
* TODO
*/
function lastUpdate(History storage self) internal view returns (uint256) {
uint256 length = self.history.length;
if (length > 0) {
return uint256(self.history[length - 1].time);
}
return 0;
}
/**
* @dev Fetch the latest registered value of history, it will return zero if there was no value registered
* @param self Checkpoints history to be queried
*/
function getLast(History storage self) internal view returns (uint256) {
uint256 length = self.history.length;
if (length > 0) {
return uint256(self.history[length - 1].value);
}
return 0;
}
/**
* @dev Fetch the most recent registered past value of a history based on a given point in time that is not known
* how recent it is beforehand. It will return zero if there is no registered value or if given time is
* previous to the first registered value.
* It uses a binary search.
* @param self Checkpoints history to be queried
* @param _time Point in time to query the most recent registered past value of
*/
function get(History storage self, uint64 _time) internal view returns (uint256) {
return _binarySearch(self, _time);
}
/**
* @dev Private function to add a new value to a history for a given point in time. This function does not allow to
* add values previous to the latest registered value, if the value willing to add corresponds to the latest
* registered value, it will be updated.
* @param self Checkpoints history to be altered
* @param _time Point in time to register the given value
* @param _value Numeric value to be registered at the given point in time
*/
function _add192(History storage self, uint64 _time, uint192 _value) private {
uint256 length = self.history.length;
if (length == 0 || self.history[self.history.length - 1].time < _time) {
// If there was no value registered or the given point in time is after the latest registered value,
// we can insert it to the history directly.
self.history.push(Checkpoint(_time, _value));
} else {
// If the point in time given for the new value is not after the latest registered value, we must ensure
// we are only trying to update the latest value, otherwise we would be changing past data.
Checkpoint storage currentCheckpoint = self.history[length - 1];
require(_time == currentCheckpoint.time, ERROR_CANNOT_ADD_PAST_VALUE);
currentCheckpoint.value = _value;
}
}
/**
* @dev Private function execute a binary search to find the most recent registered past value of a history based on
* a given point in time. It will return zero if there is no registered value or if given time is previous to
* the first registered value. Note that this function will be more suitable when don't know how recent the
* time used to index may be.
* @param self Checkpoints history to be queried
* @param _time Point in time to query the most recent registered past value of
*/
function _binarySearch(History storage self, uint64 _time) private view returns (uint256) {
// If there was no value registered for the given history return simply zero
uint256 length = self.history.length;
if (length == 0) {
return 0;
}
// If the requested time is equal to or after the time of the latest registered value, return latest value
uint256 lastIndex = length - 1;
if (_time >= self.history[lastIndex].time) {
return uint256(self.history[lastIndex].value);
}
// If the requested time is previous to the first registered value, return zero to denote missing checkpoint
if (_time < self.history[0].time) {
return 0;
}
// Execute a binary search between the checkpointed times of the history
uint256 low = 0;
uint256 high = lastIndex;
while (high > low) {
// No need for SafeMath: for this to overflow array size should be ~2^255
uint256 mid = (high + low + 1) / 2;
Checkpoint storage checkpoint = self.history[mid];
uint64 midTime = checkpoint.time;
if (_time > midTime) {
low = mid;
} else if (_time < midTime) {
// No need for SafeMath: high > low >= 0 => high >= 1 => mid >= 1
high = mid - 1;
} else {
return uint256(checkpoint.value);
}
}
return uint256(self.history[low].value);
}
}

View File

@ -0,0 +1,15 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/Autopetrified.sol
// Adapted to use pragma ^0.5.17 and satisfy our linter rules
pragma solidity ^0.5.17;
import "./Petrifiable.sol";
contract Autopetrified is Petrifiable {
constructor() public {
// Immediately petrify base (non-proxy) instances of inherited contracts on deploy.
// This renders them uninitializable (and unusable without a proxy).
petrify();
}
}

View File

@ -0,0 +1,34 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/Autopetrified.sol
// Adapted to use pragma ^0.5.17 and satisfy our linter rules
pragma solidity 0.5.17;
import "./ERCProxy.sol";
import "./IsContract.sol";
contract DelegateProxy is ERCProxy, IsContract {
uint256 internal constant FWD_GAS_LIMIT = 10000;
/**
* @dev Performs a delegatecall and returns whatever the delegatecall returned (entire context execution will return!)
* @param _dst Destination address to perform the delegatecall
* @param _calldata Calldata for the delegatecall
*/
function delegatedFwd(address _dst, bytes memory _calldata) internal {
require(isContract(_dst));
uint256 fwdGasLimit = FWD_GAS_LIMIT;
assembly {
let result := delegatecall(sub(gas, fwdGasLimit), _dst, add(_calldata, 0x20), mload(_calldata), 0, 0)
let size := returndatasize
let ptr := mload(0x40)
returndatacopy(ptr, 0, size)
// revert instead of invalid() bc if the underlying call failed with invalid() it already wasted gas.
// if the call returned error data, forward it
switch result case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}

View File

@ -0,0 +1,35 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/lib/token/ERC20.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity ^0.5.8;
/**
* @title ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/20
*/
contract ERC20 {
function totalSupply() public view returns (uint256);
function balanceOf(address _who) public view returns (uint256);
function allowance(address _owner, address _spender) public view returns (uint256);
function transfer(address _to, uint256 _value) public returns (bool);
function approve(address _spender, uint256 _value) public returns (bool);
function transferFrom(address _from, address _to, uint256 _value) public returns (bool);
event Transfer(
address indexed from,
address indexed to,
uint256 value
);
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
}

View File

@ -0,0 +1,13 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/lib/misc/ERCProxy.sol
// Adapted to use pragma ^0.5.17 and satisfy our linter rules
pragma solidity ^0.5.17;
contract ERCProxy {
uint256 internal constant FORWARDING = 1;
uint256 internal constant UPGRADEABLE = 2;
function proxyType() public pure returns (uint256 proxyTypeId);
function implementation() public view returns (address codeAddr);
}

View File

@ -0,0 +1,58 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/Initializable.sol
// Adapted to use pragma ^0.5.17 and satisfy our linter rules
pragma solidity ^0.5.17;
import "./TimeHelpers.sol";
import "./UnstructuredStorage.sol";
contract Initializable is TimeHelpers {
using UnstructuredStorage for bytes32;
// keccak256("aragonOS.initializable.initializationBlock")
bytes32 internal constant INITIALIZATION_BLOCK_POSITION = 0xebb05b386a8d34882b8711d156f463690983dc47815980fb82aeeff1aa43579e;
string private constant ERROR_ALREADY_INITIALIZED = "INIT_ALREADY_INITIALIZED";
string private constant ERROR_NOT_INITIALIZED = "INIT_NOT_INITIALIZED";
modifier onlyInit {
require(getInitializationBlock() == 0, ERROR_ALREADY_INITIALIZED);
_;
}
modifier isInitialized {
require(hasInitialized(), ERROR_NOT_INITIALIZED);
_;
}
/**
* @return Block number in which the contract was initialized
*/
function getInitializationBlock() public view returns (uint256) {
return INITIALIZATION_BLOCK_POSITION.getStorageUint256();
}
/**
* @return Whether the contract has been initialized by the time of the current block
*/
function hasInitialized() public view returns (bool) {
uint256 initializationBlock = getInitializationBlock();
return initializationBlock != 0 && getBlockNumber() >= initializationBlock;
}
/**
* @dev Function to be called by top level contract after initialization has finished.
*/
function initialized() internal onlyInit {
INITIALIZATION_BLOCK_POSITION.setStorageUint256(getBlockNumber());
}
/**
* @dev Function to be called by top level contract after initialization to enable the contract
* at a future block number rather than immediately.
*/
function initializedAt(uint256 _blockNumber) internal onlyInit {
INITIALIZATION_BLOCK_POSITION.setStorageUint256(_blockNumber);
}
}

View File

@ -0,0 +1,24 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/IsContract.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity ^0.5.8;
contract IsContract {
/*
* NOTE: this should NEVER be used for authentication
* (see pitfalls: https://github.com/fergarrui/ethereum-security/tree/master/contracts/extcodesize).
*
* This is only intended to be used as a sanity check that an address is actually a contract,
* RATHER THAN an address not being a contract.
*/
function isContract(address _target) internal view returns (bool) {
if (_target == address(0)) {
return false;
}
uint256 size;
assembly { size := extcodesize(_target) }
return size > 0;
}
}

View File

@ -0,0 +1,29 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/lib/misc/Migrations.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity ^0.5.8;
contract Migrations {
address public owner;
uint256 public lastCompletedMigration;
modifier restricted() {
if (msg.sender == owner) {
_;
}
}
constructor() public {
owner = msg.sender;
}
function setCompleted(uint256 completed) public restricted {
lastCompletedMigration = completed;
}
function upgrade(address newAddress) public restricted {
Migrations upgraded = Migrations(newAddress);
upgraded.setCompleted(lastCompletedMigration);
}
}

View File

@ -0,0 +1,24 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/Petrifiable.sol
// Adapted to use pragma ^0.5.17 and satisfy our linter rules
pragma solidity ^0.5.17;
import "./Initializable.sol";
contract Petrifiable is Initializable {
// Use block UINT256_MAX (which should be never) as the initializable date
uint256 internal constant PETRIFIED_BLOCK = uint256(-1);
function isPetrified() public view returns (bool) {
return getInitializationBlock() == PETRIFIED_BLOCK;
}
/**
* @dev Function to be called by top level contract to prevent being initialized.
* Useful for freezing base contracts when they're used behind proxies.
*/
function petrify() internal onlyInit {
initializedAt(PETRIFIED_BLOCK);
}
}

View File

@ -0,0 +1,91 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/SafeERC20.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity ^0.5.8;
import "./ERC20.sol";
library SafeERC20 {
// Before 0.5, solidity has a mismatch between `address.transfer()` and `token.transfer()`:
// https://github.com/ethereum/solidity/issues/3544
bytes4 private constant TRANSFER_SELECTOR = 0xa9059cbb;
/**
* @dev Same as a standards-compliant ERC20.transfer() that never reverts (returns false).
* Note that this makes an external call to the token.
*/
function safeTransfer(ERC20 _token, address _to, uint256 _amount) internal returns (bool) {
bytes memory transferCallData = abi.encodeWithSelector(
TRANSFER_SELECTOR,
_to,
_amount
);
return invokeAndCheckSuccess(address(_token), transferCallData);
}
/**
* @dev Same as a standards-compliant ERC20.transferFrom() that never reverts (returns false).
* Note that this makes an external call to the token.
*/
function safeTransferFrom(ERC20 _token, address _from, address _to, uint256 _amount) internal returns (bool) {
bytes memory transferFromCallData = abi.encodeWithSelector(
_token.transferFrom.selector,
_from,
_to,
_amount
);
return invokeAndCheckSuccess(address(_token), transferFromCallData);
}
/**
* @dev Same as a standards-compliant ERC20.approve() that never reverts (returns false).
* Note that this makes an external call to the token.
*/
function safeApprove(ERC20 _token, address _spender, uint256 _amount) internal returns (bool) {
bytes memory approveCallData = abi.encodeWithSelector(
_token.approve.selector,
_spender,
_amount
);
return invokeAndCheckSuccess(address(_token), approveCallData);
}
function invokeAndCheckSuccess(address _addr, bytes memory _calldata) private returns (bool) {
bool ret;
assembly {
let ptr := mload(0x40) // free memory pointer
let success := call(
gas, // forward all gas
_addr, // address
0, // no value
add(_calldata, 0x20), // calldata start
mload(_calldata), // calldata length
ptr, // write output over free memory
0x20 // uint256 return
)
if gt(success, 0) {
// Check number of bytes returned from last function call
switch returndatasize
// No bytes returned: assume success
case 0 {
ret := 1
}
// 32 bytes returned: check if non-zero
case 0x20 {
// Only return success if returned data was true
// Already have output in ptr
ret := eq(mload(ptr), 1)
}
// Not sure what was returned: don't mark as success
default { }
}
}
return ret;
}
}

View File

@ -0,0 +1,73 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/lib/math/SafeMath.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity >=0.4.24 <0.6.0;
/**
* @title SafeMath
* @dev Math operations with safety checks that revert on error
*/
library SafeMath {
string private constant ERROR_ADD_OVERFLOW = "MATH_ADD_OVERFLOW";
string private constant ERROR_SUB_UNDERFLOW = "MATH_SUB_UNDERFLOW";
string private constant ERROR_MUL_OVERFLOW = "MATH_MUL_OVERFLOW";
string private constant ERROR_DIV_ZERO = "MATH_DIV_ZERO";
/**
* @dev Multiplies two numbers, reverts on overflow.
*/
function mul(uint256 _a, uint256 _b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (_a == 0) {
return 0;
}
uint256 c = _a * _b;
require(c / _a == _b, ERROR_MUL_OVERFLOW);
return c;
}
/**
* @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
*/
function div(uint256 _a, uint256 _b) internal pure returns (uint256) {
require(_b > 0, ERROR_DIV_ZERO); // Solidity only automatically asserts when dividing by 0
uint256 c = _a / _b;
// assert(_a == _b * c + _a % _b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 _a, uint256 _b) internal pure returns (uint256) {
require(_b <= _a, ERROR_SUB_UNDERFLOW);
uint256 c = _a - _b;
return c;
}
/**
* @dev Adds two numbers, reverts on overflow.
*/
function add(uint256 _a, uint256 _b) internal pure returns (uint256) {
uint256 c = _a + _b;
require(c >= _a, ERROR_ADD_OVERFLOW);
return c;
}
/**
* @dev Divides two numbers and returns the remainder (unsigned integer modulo),
* reverts when dividing by zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0, ERROR_DIV_ZERO);
return a % b;
}
}

View File

@ -0,0 +1,66 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/lib/math/SafeMath64.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity ^0.5.8;
/**
* @title SafeMath64
* @dev Math operations for uint64 with safety checks that revert on error
*/
library SafeMath64 {
string private constant ERROR_ADD_OVERFLOW = "MATH64_ADD_OVERFLOW";
string private constant ERROR_SUB_UNDERFLOW = "MATH64_SUB_UNDERFLOW";
string private constant ERROR_MUL_OVERFLOW = "MATH64_MUL_OVERFLOW";
string private constant ERROR_DIV_ZERO = "MATH64_DIV_ZERO";
/**
* @dev Multiplies two numbers, reverts on overflow.
*/
function mul(uint64 _a, uint64 _b) internal pure returns (uint64) {
uint256 c = uint256(_a) * uint256(_b);
require(c < 0x010000000000000000, ERROR_MUL_OVERFLOW); // 2**64 (less gas this way)
return uint64(c);
}
/**
* @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
*/
function div(uint64 _a, uint64 _b) internal pure returns (uint64) {
require(_b > 0, ERROR_DIV_ZERO); // Solidity only automatically asserts when dividing by 0
uint64 c = _a / _b;
// assert(_a == _b * c + _a % _b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint64 _a, uint64 _b) internal pure returns (uint64) {
require(_b <= _a, ERROR_SUB_UNDERFLOW);
uint64 c = _a - _b;
return c;
}
/**
* @dev Adds two numbers, reverts on overflow.
*/
function add(uint64 _a, uint64 _b) internal pure returns (uint64) {
uint64 c = _a + _b;
require(c >= _a, ERROR_ADD_OVERFLOW);
return c;
}
/**
* @dev Divides two numbers and returns the remainder (unsigned integer modulo),
* reverts when dividing by zero.
*/
function mod(uint64 a, uint64 b) internal pure returns (uint64) {
require(b != 0, ERROR_DIV_ZERO);
return a % b;
}
}

View File

@ -0,0 +1,47 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/evmscript/ScriptHelpers.sol
// Adapted to use pragma ^0.5.17 and satisfy our linter rules
pragma solidity ^0.5.17;
library ScriptHelpers {
function getSpecId(bytes memory _script) internal pure returns (uint32) {
return uint32At(_script, 0);
}
function uint256At(bytes memory _data, uint256 _location) internal pure returns (uint256 result) {
assembly {
result := mload(add(_data, add(0x20, _location)))
}
}
function addressAt(bytes memory _data, uint256 _location) internal pure returns (address result) {
uint256 word = uint256At(_data, _location);
assembly {
result := div(and(word, 0xffffffffffffffffffffffffffffffffffffffff000000000000000000000000),
0x1000000000000000000000000)
}
}
function uint32At(bytes memory _data, uint256 _location) internal pure returns (uint32 result) {
uint256 word = uint256At(_data, _location);
assembly {
result := div(and(word, 0xffffffff00000000000000000000000000000000000000000000000000000000),
0x100000000000000000000000000000000000000000000000000000000)
}
}
function locationOf(bytes memory _data, uint256 _location) internal pure returns (uint256 result) {
assembly {
result := add(_data, add(0x20, _location))
}
}
function toBytes(bytes4 _sig) internal pure returns (bytes memory) {
bytes memory payload = new bytes(4);
assembly { mstore(add(payload, 0x20), _sig) }
return payload;
}
}

View File

@ -0,0 +1,47 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/TimeHelpers.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity ^0.5.8;
import "./Uint256Helpers.sol";
contract TimeHelpers {
using Uint256Helpers for uint256;
/**
* @dev Returns the current block number.
* Using a function rather than `block.number` allows us to easily mock the block number in
* tests.
*/
function getBlockNumber() internal view returns (uint256) {
return block.number;
}
/**
* @dev Returns the current block number, converted to uint64.
* Using a function rather than `block.number` allows us to easily mock the block number in
* tests.
*/
function getBlockNumber64() internal view returns (uint64) {
return getBlockNumber().toUint64();
}
/**
* @dev Returns the current timestamp.
* Using a function rather than `block.timestamp` allows us to easily mock it in
* tests.
*/
function getTimestamp() internal view returns (uint256) {
return block.timestamp; // solium-disable-line security/no-block-members
}
/**
* @dev Returns the current timestamp, converted to uint64.
* Using a function rather than `block.timestamp` allows us to easily mock it in
* tests.
*/
function getTimestamp64() internal view returns (uint64) {
return getTimestamp().toUint64();
}
}

View File

@ -0,0 +1,23 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/Uint256Helpers.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity ^0.5.8;
library Uint256Helpers {
uint256 private constant MAX_UINT8 = uint8(-1);
uint256 private constant MAX_UINT64 = uint64(-1);
string private constant ERROR_UINT8_NUMBER_TOO_BIG = "UINT8_NUMBER_TOO_BIG";
string private constant ERROR_UINT64_NUMBER_TOO_BIG = "UINT64_NUMBER_TOO_BIG";
function toUint8(uint256 a) internal pure returns (uint8) {
require(a <= MAX_UINT8, ERROR_UINT8_NUMBER_TOO_BIG);
return uint8(a);
}
function toUint64(uint256 a) internal pure returns (uint64) {
require(a <= MAX_UINT64, ERROR_UINT64_NUMBER_TOO_BIG);
return uint64(a);
}
}

View File

@ -0,0 +1,39 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/UnstructuredStorage.sol
// Adapted to use pragma ^0.5.17 and satisfy our linter rules
pragma solidity ^0.5.17;
library UnstructuredStorage {
function getStorageBool(bytes32 position) internal view returns (bool data) {
assembly { data := sload(position) }
}
function getStorageAddress(bytes32 position) internal view returns (address data) {
assembly { data := sload(position) }
}
function getStorageBytes32(bytes32 position) internal view returns (bytes32 data) {
assembly { data := sload(position) }
}
function getStorageUint256(bytes32 position) internal view returns (uint256 data) {
assembly { data := sload(position) }
}
function setStorageBool(bytes32 position, bool data) internal {
assembly { sstore(position, data) }
}
function setStorageAddress(bytes32 position, address data) internal {
assembly { sstore(position, data) }
}
function setStorageBytes32(bytes32 position, bytes32 data) internal {
assembly { sstore(position, data) }
}
function setStorageUint256(bytes32 position, uint256 data) internal {
assembly { sstore(position, data) }
}
}

View File

@ -0,0 +1,12 @@
pragma solidity ^0.5.17;
interface ILockManager {
/**
* @notice Check if `_user`'s by `_lockManager` can be unlocked
* @param _user Owner of lock
* @param _amount Amount of locked tokens to unlock
* @return Whether given lock of given owner can be unlocked by given sender
*/
function canUnlock(address _user, uint256 _amount) external view returns (bool);
}

View File

@ -0,0 +1,31 @@
pragma solidity ^0.5.17;
interface IStakingLocking {
event NewLockManager(address indexed account, address indexed lockManager, bytes data);
event Unlocked(address indexed account, address indexed lockManager, uint256 amount);
event LockAmountChanged(address indexed account, address indexed lockManager, uint256 amount, bool increase);
event LockAllowanceChanged(address indexed account, address indexed lockManager, uint256 allowance, bool increase);
event LockManagerRemoved(address indexed account, address lockManager);
event LockManagerTransferred(address indexed account, address indexed oldLockManager, address newLockManager);
event StakeTransferred(address indexed from, address to, uint256 amount);
function allowManager(address _lockManager, uint256 _allowance, bytes calldata _data) external;
function allowManagerAndLock(uint256 _amount, address _lockManager, uint256 _allowance, bytes calldata _data) external;
function unlockAndRemoveManager(address _account, address _lockManager) external;
function increaseLockAllowance(address _lockManager, uint256 _allowance) external;
function decreaseLockAllowance(address _account, address _lockManager, uint256 _allowance) external;
function lock(address _account, address _lockManager, uint256 _amount) external;
function unlock(address _account, address _lockManager, uint256 _amount) external;
function setLockManager(address _account, address _newLockManager) external;
function transfer(address _to, uint256 _amount) external;
function transferAndUnstake(address _to, uint256 _amount) external;
function slash(address _account, address _to, uint256 _amount) external;
function slashAndUnstake(address _account, address _to, uint256 _amount) external;
function getLock(address _account, address _lockManager) external view returns (uint256 _amount, uint256 _allowance);
function unlockedBalanceOf(address _account) external view returns (uint256);
function lockedBalanceOf(address _user) external view returns (uint256);
function getBalancesOf(address _user) external view returns (uint256 staked, uint256 locked);
function canUnlock(address _sender, address _account, address _lockManager, uint256 _amount) external view returns (bool);
}

View File

@ -0,0 +1,72 @@
pragma solidity 0.5.17;
import "../lib/os/TimeHelpers.sol";
import "../lib/os/ScriptHelpers.sol";
import "../locking/ILockManager.sol";
import "../locking/IStakingLocking.sol";
/**
* Time based lock manager for Staking contract
* Allows to set a time interval, either in blocks or seconds, during which the funds are locked.
* Outside that window the owner can unlock them.
*/
contract TimeLockManager is ILockManager, TimeHelpers {
using ScriptHelpers for bytes;
string private constant ERROR_ALREADY_LOCKED = "TLM_ALREADY_LOCKED";
string private constant ERROR_WRONG_INTERVAL = "TLM_WRONG_INTERVAL";
enum TimeUnit { Blocks, Seconds }
struct TimeInterval {
uint256 unit;
uint256 start;
uint256 end;
}
mapping (address => TimeInterval) internal timeIntervals;
event LogLockCallback(uint256 amount, uint256 allowance, bytes data);
/**
* @notice Set a locked amount, along with a time interval, either in blocks or seconds during which the funds are locked.
* @param _staking The Staking contract holding the lock
* @param _owner The account owning the locked funds
* @param _amount The amount to be locked
* @param _unit Blocks or seconds, the unit for the time interval
* @param _start The start of the time interval
* @param _end The end of the time interval
*/
function lock(IStakingLocking _staking, address _owner, uint256 _amount, uint256 _unit, uint256 _start, uint256 _end) external {
require(timeIntervals[_owner].end == 0, ERROR_ALREADY_LOCKED);
require(_end > _start, ERROR_WRONG_INTERVAL);
timeIntervals[_owner] = TimeInterval(_unit, _start, _end);
_staking.lock(_owner, address(this), _amount);
}
/**
* @notice Check if the owner can unlock the funds, i.e., if current timestamp is outside the lock interval
* @param _owner Owner of the locked funds
* @return True if current timestamp is outside the lock interval
*/
function canUnlock(address _owner, uint256) external view returns (bool) {
TimeInterval storage timeInterval = timeIntervals[_owner];
uint256 comparingValue;
if (timeInterval.unit == uint256(TimeUnit.Blocks)) {
comparingValue = getBlockNumber();
} else {
comparingValue = getTimestamp();
}
return comparingValue < timeInterval.start || comparingValue > timeInterval.end;
}
function getTimeInterval(address _owner) external view returns (uint256 unit, uint256 start, uint256 end) {
TimeInterval storage timeInterval = timeIntervals[_owner];
return (timeInterval.unit, timeInterval.start, timeInterval.end);
}
}

View File

@ -0,0 +1,31 @@
pragma solidity ^0.5.17;
import "../lib/os/ERC20.sol";
import "../Staking.sol";
import "./ThinProxy.sol";
contract StakingProxy is ThinProxy {
// keccak256("aragon.network.staking")
bytes32 internal constant IMPLEMENTATION_SLOT = 0xbd536e2e005accda865e2f0d1827f83ec8824f3ea04ecd6131b7c10058635814;
constructor(Staking _implementation, ERC20 _token) ThinProxy(address(_implementation)) public {
bytes4 selector = _implementation.initialize.selector;
bytes memory initializeData = abi.encodeWithSelector(selector, _token);
(bool success,) = address(_implementation).delegatecall(initializeData);
if (!success) {
assembly {
let output := mload(0x40)
mstore(0x40, add(output, returndatasize))
returndatacopy(output, 0, returndatasize)
revert(output, returndatasize)
}
}
}
function _implementationSlot() internal pure returns (bytes32) {
return IMPLEMENTATION_SLOT;
}
}

View File

@ -0,0 +1,27 @@
pragma solidity ^0.5.17;
import "../lib/os/DelegateProxy.sol";
import "../lib/os/UnstructuredStorage.sol";
contract ThinProxy is DelegateProxy {
using UnstructuredStorage for bytes32;
constructor(address _implementation) public {
_implementationSlot().setStorageAddress(_implementation);
}
function () external {
delegatedFwd(implementation(), msg.data);
}
function proxyType() public pure returns (uint256) {
return FORWARDING;
}
function implementation() public view returns (address) {
return _implementationSlot().getStorageAddress();
}
function _implementationSlot() internal pure returns (bytes32);
}

View File

@ -0,0 +1,55 @@
pragma solidity ^0.5.17;
// Interface for ERC900: https://eips.ethereum.org/EIPS/eip-900
interface ERC900 {
event Staked(address indexed user, uint256 amount, uint256 total, bytes data);
event Unstaked(address indexed user, uint256 amount, uint256 total, bytes data);
/**
* @dev Stake a certain amount of tokens
* @param _amount Amount of tokens to be staked
* @param _data Optional data that can be used to add signalling information in more complex staking applications
*/
function stake(uint256 _amount, bytes calldata _data) external;
/**
* @dev Stake a certain amount of tokens in favor of someone
* @param _user Address to stake an amount of tokens to
* @param _amount Amount of tokens to be staked
* @param _data Optional data that can be used to add signalling information in more complex staking applications
*/
function stakeFor(address _user, uint256 _amount, bytes calldata _data) external;
/**
* @dev Unstake a certain amount of tokens
* @param _amount Amount of tokens to be unstaked
* @param _data Optional data that can be used to add signalling information in more complex staking applications
*/
function unstake(uint256 _amount, bytes calldata _data) external;
/**
* @dev Tell the total amount of tokens staked for an address
* @param _addr Address querying the total amount of tokens staked for
* @return Total amount of tokens staked for an address
*/
function totalStakedFor(address _addr) external view returns (uint256);
/**
* @dev Tell the total amount of tokens staked
* @return Total amount of tokens staked
*/
function totalStaked() external view returns (uint256);
/**
* @dev Tell the address of the token used for staking
* @return Address of the token used for staking
*/
function token() external view returns (address);
/*
* @dev Tell if the current registry supports historic information or not
* @return True if the optional history functions are implemented, false otherwise
*/
function supportsHistory() external pure returns (bool);
}

View File

@ -0,0 +1,23 @@
pragma solidity 0.5.17;
import "./lib/MiniMeToken.sol";
import "../lib/os/Migrations.sol";
// You might think this file is a bit odd, but let me explain.
// We only use some contracts in our tests, which means Truffle
// will not compile it for us, because it is from an external
// dependency.
//
// We are now left with three options:
// - Copy/paste these contracts
// - Run the tests with `truffle compile --all` on
// - Or trick Truffle by claiming we use it in a Solidity test
//
// You know which one I went for.
contract TestImports {
constructor() public {
// solium-disable-previous-line no-empty-blocks
}
}

View File

@ -0,0 +1,88 @@
pragma solidity 0.5.17;
import "../../Staking.sol";
import "../../lib/os/SafeMath.sol";
import "../mocks/NoApproveTokenMock.sol";
contract EchidnaStaking is Staking {
using SafeMath for uint256;
constructor() public {
stakingToken = ERC20(new NoApproveTokenMock(msg.sender, 10 ** 24));
}
// check that staked amount for an account is always >= total locked
function echidna_account_stake_locks() external view returns (bool) {
address _account = msg.sender;
Account storage account = accounts[_account];
if (_totalStakedFor(_account) < account.totalLocked) {
return false;
}
return true;
}
// TODO: delete. Fake test to check that previous echidna test works
function echidna_account_stake_locks_fake() external view returns (bool) {
address _account = msg.sender;
Account storage account = accounts[_account];
if (_totalStakedFor(_account) > account.totalLocked) {
return false;
}
return true;
}
// check that Checkpointing history arrays are ordered
function echidna_global_history_is_ordered() external view returns (bool) {
for (uint256 i = 1; i < totalStakedHistory.history.length; i++) {
if (totalStakedHistory.history[i].time <= totalStakedHistory.history[i - 1].time) {
return false;
}
}
return true;
}
function echidna_user_history_is_ordered() external view returns (bool) {
address account = msg.sender;
for (uint256 i = 1; i < accounts[account].stakedHistory.history.length; i++) {
if (accounts[account].stakedHistory.history[i].time <= accounts[account].stakedHistory.history[i - 1].time) {
return false;
}
}
return true;
}
// total staked matches less or equal than token balance
function echidna_total_staked_is_balance() external view returns (bool) {
if (_totalStaked() <= stakingToken.balanceOf(address(this))) {
return true;
}
return false;
}
function echidna_staked_ge_unlocked() external view returns (bool) {
if (_unlockedBalanceOf(msg.sender) > _totalStakedFor(msg.sender)) {
return false;
}
return true;
}
function echidna_staked_ge_locked() external view returns (bool) {
if (_lockedBalanceOf(msg.sender) > _totalStakedFor(msg.sender)) {
return false;
}
return true;
}
// sum of all account stakes should be equal to total staked and to staking token balance of staking contract, but it's hard to compute as accounts is a mapping
}

View File

@ -0,0 +1,27 @@
pragma solidity ^0.5.17;
/// @dev The token controller contract must implement these functions
interface ITokenController {
/// @notice Called when `_owner` sends ether to the MiniMe Token contract
/// @param _owner The address that sent the ether to create tokens
/// @return True if the ether is accepted, false if it throws
function proxyPayment(address _owner) external payable returns(bool);
/// @notice Notifies the controller about a token transfer allowing the
/// controller to react if desired
/// @param _from The origin of the transfer
/// @param _to The destination of the transfer
/// @param _amount The amount of the transfer
/// @return False if the controller does not authorize the transfer
function onTransfer(address _from, address _to, uint _amount) external returns(bool);
/// @notice Notifies the controller about an approval allowing the
/// controller to react if desired
/// @param _owner The address that calls `approve()`
/// @param _spender The spender in the `approve()` call
/// @param _amount The amount in the `approve()` call
/// @return False if the controller does not authorize the approval
function onApprove(address _owner, address _spender, uint _amount) external returns(bool);
}

View File

@ -0,0 +1,576 @@
pragma solidity ^0.5.17;
/*
Copyright 2016, Jordi Baylina
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/// @title MiniMeToken Contract
/// @author Jordi Baylina
/// @dev This token contract's goal is to make it easy for anyone to clone this
/// token using the token distribution at a given block, this will allow DAO's
/// and DApps to upgrade their features in a decentralized manner without
/// affecting the original token
/// @dev It is ERC20 compliant, but still needs to under go further testing.
import "./ITokenController.sol";
contract Controlled {
/// @notice The address of the controller is the only address that can call
/// a function with this modifier
modifier onlyController {
require(msg.sender == controller);
_;
}
address payable public controller;
constructor() public { controller = msg.sender; }
/// @notice Changes the controller of the contract
/// @param _newController The new controller of the contract
function changeController(address payable _newController) onlyController public {
controller = _newController;
}
}
contract ApproveAndCallFallBack {
function receiveApproval(
address from,
uint256 _amount,
address _token,
bytes calldata _data
) external;
}
/// @dev The actual token contract, the default controller is the msg.sender
/// that deploys the contract, so usually this token will be deployed by a
/// token controller contract, which Giveth will call a "Campaign"
contract MiniMeToken is Controlled {
string public name; //The Token's name: e.g. DigixDAO Tokens
uint8 public decimals; //Number of decimals of the smallest unit
string public symbol; //An identifier: e.g. REP
string public version = "MMT_0.1"; //An arbitrary versioning scheme
/// @dev `Checkpoint` is the structure that attaches a block number to a
/// given value, the block number attached is the one that last changed the
/// value
struct Checkpoint {
// `fromBlock` is the block number that the value was generated from
uint128 fromBlock;
// `value` is the amount of tokens at a specific block number
uint128 value;
}
// `parentToken` is the Token address that was cloned to produce this token;
// it will be 0x0 for a token that was not cloned
MiniMeToken public parentToken;
// `parentSnapShotBlock` is the block number from the Parent Token that was
// used to determine the initial distribution of the Clone Token
uint public parentSnapShotBlock;
// `creationBlock` is the block number that the Clone Token was created
uint public creationBlock;
// `balances` is the map that tracks the balance of each address, in this
// contract when the balance changes the block number that the change
// occurred is also included in the map
mapping (address => Checkpoint[]) balances;
// `allowed` tracks any extra transfer rights as in all ERC20 tokens
mapping (address => mapping (address => uint256)) allowed;
// Tracks the history of the `totalSupply` of the token
Checkpoint[] totalSupplyHistory;
// Flag that determines if the token is transferable or not.
bool public transfersEnabled;
// The factory used to create new clone tokens
MiniMeTokenFactory public tokenFactory;
////////////////
// Constructor
////////////////
/// @notice Constructor to create a MiniMeToken
/// @param _tokenFactory The address of the MiniMeTokenFactory contract that
/// will create the Clone token contracts, the token factory needs to be
/// deployed first
/// @param _parentToken Address of the parent token, set to 0x0 if it is a
/// new token
/// @param _parentSnapShotBlock Block of the parent token that will
/// determine the initial distribution of the clone token, set to 0 if it
/// is a new token
/// @param _tokenName Name of the new token
/// @param _decimalUnits Number of decimals of the new token
/// @param _tokenSymbol Token Symbol for the new token
/// @param _transfersEnabled If true, tokens will be able to be transferred
constructor(
MiniMeTokenFactory _tokenFactory,
MiniMeToken _parentToken,
uint _parentSnapShotBlock,
string memory _tokenName,
uint8 _decimalUnits,
string memory _tokenSymbol,
bool _transfersEnabled
) public
{
tokenFactory = _tokenFactory;
name = _tokenName; // Set the name
decimals = _decimalUnits; // Set the decimals
symbol = _tokenSymbol; // Set the symbol
parentToken = _parentToken;
parentSnapShotBlock = _parentSnapShotBlock;
transfersEnabled = _transfersEnabled;
creationBlock = block.number;
}
///////////////////
// ERC20 Methods
///////////////////
/// @notice Send `_amount` tokens to `_to` from `msg.sender`
/// @param _to The address of the recipient
/// @param _amount The amount of tokens to be transferred
/// @return Whether the transfer was successful or not
function transfer(address _to, uint256 _amount) public returns (bool success) {
require(transfersEnabled);
return doTransfer(msg.sender, _to, _amount);
}
/// @notice Send `_amount` tokens to `_to` from `_from` on the condition it
/// is approved by `_from`
/// @param _from The address holding the tokens being transferred
/// @param _to The address of the recipient
/// @param _amount The amount of tokens to be transferred
/// @return True if the transfer was successful
function transferFrom(address _from, address _to, uint256 _amount) public returns (bool success) {
// The controller of this contract can move tokens around at will,
// this is important to recognize! Confirm that you trust the
// controller of this contract, which in most situations should be
// another open source smart contract or 0x0
if (msg.sender != controller) {
require(transfersEnabled);
// The standard ERC 20 transferFrom functionality
if (allowed[_from][msg.sender] < _amount)
return false;
allowed[_from][msg.sender] -= _amount;
}
return doTransfer(_from, _to, _amount);
}
/// @dev This is the actual transfer function in the token contract, it can
/// only be called by other functions in this contract.
/// @param _from The address holding the tokens being transferred
/// @param _to The address of the recipient
/// @param _amount The amount of tokens to be transferred
/// @return True if the transfer was successful
function doTransfer(address _from, address _to, uint _amount) internal returns(bool) {
if (_amount == 0) {
return true;
}
require(parentSnapShotBlock < block.number);
// Do not allow transfer to 0x0 or the token contract itself
require((_to != address(0)) && (_to != address(this)));
// If the amount being transfered is more than the balance of the
// account the transfer returns false
uint256 previousBalanceFrom = balanceOfAt(_from, block.number);
if (previousBalanceFrom < _amount) {
return false;
}
// Alerts the token controller of the transfer
if (isContract(controller)) {
// Adding the ` == true` makes the linter shut up so...
require(ITokenController(controller).onTransfer(_from, _to, _amount) == true);
}
// First update the balance array with the new value for the address
// sending the tokens
updateValueAtNow(balances[_from], previousBalanceFrom - _amount);
// Then update the balance array with the new value for the address
// receiving the tokens
uint256 previousBalanceTo = balanceOfAt(_to, block.number);
require(previousBalanceTo + _amount >= previousBalanceTo); // Check for overflow
updateValueAtNow(balances[_to], previousBalanceTo + _amount);
// An event to make the transfer easy to find on the blockchain
emit Transfer(_from, _to, _amount);
return true;
}
/// @param _owner The address that's balance is being requested
/// @return The balance of `_owner` at the current block
function balanceOf(address _owner) public view returns (uint256 balance) {
return balanceOfAt(_owner, block.number);
}
/// @notice `msg.sender` approves `_spender` to spend `_amount` tokens on
/// its behalf. This is a modified version of the ERC20 approve function
/// to be a little bit safer
/// @param _spender The address of the account able to transfer the tokens
/// @param _amount The amount of tokens to be approved for transfer
/// @return True if the approval was successful
function approve(address _spender, uint256 _amount) public returns (bool success) {
require(transfersEnabled);
// To change the approve amount you first have to reduce the addresses`
// allowance to zero by calling `approve(_spender,0)` if it is not
// already 0 to mitigate the race condition described here:
// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
require((_amount == 0) || (allowed[msg.sender][_spender] == 0));
// Alerts the token controller of the approve function call
if (isContract(controller)) {
// Adding the ` == true` makes the linter shut up so...
require(ITokenController(controller).onApprove(msg.sender, _spender, _amount) == true);
}
allowed[msg.sender][_spender] = _amount;
emit Approval(msg.sender, _spender, _amount);
return true;
}
/// @dev This function makes it easy to read the `allowed[]` map
/// @param _owner The address of the account that owns the token
/// @param _spender The address of the account able to transfer the tokens
/// @return Amount of remaining tokens of _owner that _spender is allowed
/// to spend
function allowance(address _owner, address _spender) public view returns (uint256 remaining) {
return allowed[_owner][_spender];
}
/// @notice `msg.sender` approves `_spender` to send `_amount` tokens on
/// its behalf, and then a function is triggered in the contract that is
/// being approved, `_spender`. This allows users to use their tokens to
/// interact with contracts in one function call instead of two
/// @param _spender The address of the contract able to transfer the tokens
/// @param _amount The amount of tokens to be approved for transfer
/// @return True if the function call was successful
function approveAndCall(ApproveAndCallFallBack _spender, uint256 _amount, bytes calldata _extraData) external returns (bool success) {
require(approve(address(_spender), _amount));
_spender.receiveApproval(
msg.sender,
_amount,
address(this),
_extraData
);
return true;
}
/// @dev This function makes it easy to get the total number of tokens
/// @return The total number of tokens
function totalSupply() public view returns (uint) {
return totalSupplyAt(block.number);
}
////////////////
// Query balance and totalSupply in History
////////////////
/// @dev Queries the balance of `_owner` at a specific `_blockNumber`
/// @param _owner The address from which the balance will be retrieved
/// @param _blockNumber The block number when the balance is queried
/// @return The balance at `_blockNumber`
function balanceOfAt(address _owner, uint _blockNumber) public view returns (uint) {
// These next few lines are used when the balance of the token is
// requested before a check point was ever created for this token, it
// requires that the `parentToken.balanceOfAt` be queried at the
// genesis block for that token as this contains initial balance of
// this token
if ((balances[_owner].length == 0) || (balances[_owner][0].fromBlock > _blockNumber)) {
if (address(parentToken) != address(0)) {
return parentToken.balanceOfAt(_owner, min(_blockNumber, parentSnapShotBlock));
} else {
// Has no parent
return 0;
}
// This will return the expected balance during normal situations
} else {
return getValueAt(balances[_owner], _blockNumber);
}
}
/// @notice Total amount of tokens at a specific `_blockNumber`.
/// @param _blockNumber The block number when the totalSupply is queried
/// @return The total amount of tokens at `_blockNumber`
function totalSupplyAt(uint _blockNumber) public view returns(uint) {
// These next few lines are used when the totalSupply of the token is
// requested before a check point was ever created for this token, it
// requires that the `parentToken.totalSupplyAt` be queried at the
// genesis block for this token as that contains totalSupply of this
// token at this block number.
if ((totalSupplyHistory.length == 0) || (totalSupplyHistory[0].fromBlock > _blockNumber)) {
if (address(parentToken) != address(0)) {
return parentToken.totalSupplyAt(min(_blockNumber, parentSnapShotBlock));
} else {
return 0;
}
// This will return the expected totalSupply during normal situations
} else {
return getValueAt(totalSupplyHistory, _blockNumber);
}
}
////////////////
// Clone Token Method
////////////////
/// @notice Creates a new clone token with the initial distribution being
/// this token at `_snapshotBlock`
/// @param _cloneTokenName Name of the clone token
/// @param _cloneDecimalUnits Number of decimals of the smallest unit
/// @param _cloneTokenSymbol Symbol of the clone token
/// @param _snapshotBlock Block when the distribution of the parent token is
/// copied to set the initial distribution of the new clone token;
/// if the block is zero than the actual block, the current block is used
/// @param _transfersEnabled True if transfers are allowed in the clone
/// @return The address of the new MiniMeToken Contract
function createCloneToken(
string calldata _cloneTokenName,
uint8 _cloneDecimalUnits,
string calldata _cloneTokenSymbol,
uint _snapshotBlock,
bool _transfersEnabled
) external returns(MiniMeToken)
{
uint256 snapshot = _snapshotBlock == 0 ? block.number - 1 : _snapshotBlock;
MiniMeToken cloneToken = tokenFactory.createCloneToken(
this,
snapshot,
_cloneTokenName,
_cloneDecimalUnits,
_cloneTokenSymbol,
_transfersEnabled
);
cloneToken.changeController(msg.sender);
// An event to make the token easy to find on the blockchain
emit NewCloneToken(address(cloneToken), snapshot);
return cloneToken;
}
////////////////
// Generate and destroy tokens
////////////////
/// @notice Generates `_amount` tokens that are assigned to `_owner`
/// @param _owner The address that will be assigned the new tokens
/// @param _amount The quantity of tokens generated
/// @return True if the tokens are generated correctly
function generateTokens(address _owner, uint _amount) onlyController public returns (bool) {
uint curTotalSupply = totalSupply();
require(curTotalSupply + _amount >= curTotalSupply); // Check for overflow
uint previousBalanceTo = balanceOf(_owner);
require(previousBalanceTo + _amount >= previousBalanceTo); // Check for overflow
updateValueAtNow(totalSupplyHistory, curTotalSupply + _amount);
updateValueAtNow(balances[_owner], previousBalanceTo + _amount);
emit Transfer(address(0), _owner, _amount);
return true;
}
/// @notice Burns `_amount` tokens from `_owner`
/// @param _owner The address that will lose the tokens
/// @param _amount The quantity of tokens to burn
/// @return True if the tokens are burned correctly
function destroyTokens(address _owner, uint _amount) onlyController public returns (bool) {
uint curTotalSupply = totalSupply();
require(curTotalSupply >= _amount);
uint previousBalanceFrom = balanceOf(_owner);
require(previousBalanceFrom >= _amount);
updateValueAtNow(totalSupplyHistory, curTotalSupply - _amount);
updateValueAtNow(balances[_owner], previousBalanceFrom - _amount);
emit Transfer(_owner, address(0), _amount);
return true;
}
////////////////
// Enable tokens transfers
////////////////
/// @notice Enables token holders to transfer their tokens freely if true
/// @param _transfersEnabled True if transfers are allowed in the clone
function enableTransfers(bool _transfersEnabled) onlyController public {
transfersEnabled = _transfersEnabled;
}
////////////////
// Internal helper functions to query and set a value in a snapshot array
////////////////
/// @dev `getValueAt` retrieves the number of tokens at a given block number
/// @param checkpoints The history of values being queried
/// @param _block The block number to retrieve the value at
/// @return The number of tokens being queried
function getValueAt(Checkpoint[] storage checkpoints, uint _block) view internal returns (uint) {
if (checkpoints.length == 0)
return 0;
// Shortcut for the actual value
if (_block >= checkpoints[checkpoints.length-1].fromBlock)
return checkpoints[checkpoints.length-1].value;
if (_block < checkpoints[0].fromBlock)
return 0;
// Binary search of the value in the array
uint min = 0;
uint max = checkpoints.length-1;
while (max > min) {
uint mid = (max + min + 1) / 2;
if (checkpoints[mid].fromBlock<=_block) {
min = mid;
} else {
max = mid-1;
}
}
return checkpoints[min].value;
}
/// @dev `updateValueAtNow` used to update the `balances` map and the
/// `totalSupplyHistory`
/// @param checkpoints The history of data being updated
/// @param _value The new number of tokens
function updateValueAtNow(Checkpoint[] storage checkpoints, uint _value) internal {
if ((checkpoints.length == 0) || (checkpoints[checkpoints.length - 1].fromBlock < block.number)) {
Checkpoint storage newCheckPoint = checkpoints[checkpoints.length++];
newCheckPoint.fromBlock = uint128(block.number);
newCheckPoint.value = uint128(_value);
} else {
Checkpoint storage oldCheckPoint = checkpoints[checkpoints.length - 1];
oldCheckPoint.value = uint128(_value);
}
}
/// @dev Internal function to determine if an address is a contract
/// @param _addr The address being queried
/// @return True if `_addr` is a contract
function isContract(address _addr) view internal returns(bool) {
uint size;
if (_addr == address(0))
return false;
assembly {
size := extcodesize(_addr)
}
return size>0;
}
/// @dev Helper function to return a min betwen the two uints
function min(uint a, uint b) pure internal returns (uint) {
return a < b ? a : b;
}
/// @notice The fallback function: If the contract's controller has not been
/// set to 0, then the `proxyPayment` method is called which relays the
/// ether and creates tokens as described in the token controller contract
function () external payable {
require(isContract(controller));
// Adding the ` == true` makes the linter shut up so...
require(ITokenController(controller).proxyPayment.value(msg.value)(msg.sender) == true);
}
//////////
// Safety Methods
//////////
/// @notice This method can be used by the controller to extract mistakenly
/// sent tokens to this contract.
/// @param _token The address of the token contract that you want to recover
/// set to 0 in case you want to extract ether.
function claimTokens(address payable _token) onlyController public {
if (_token == address(0)) {
controller.transfer(address(this).balance);
return;
}
MiniMeToken token = MiniMeToken(_token);
uint balance = token.balanceOf(address(this));
token.transfer(controller, balance);
emit ClaimedTokens(_token, controller, balance);
}
////////////////
// Events
////////////////
event ClaimedTokens(address indexed _token, address indexed _controller, uint _amount);
event Transfer(address indexed _from, address indexed _to, uint256 _amount);
event NewCloneToken(address indexed _cloneToken, uint _snapshotBlock);
event Approval(
address indexed _owner,
address indexed _spender,
uint256 _amount
);
}
////////////////
// MiniMeTokenFactory
////////////////
/// @dev This contract is used to generate clone contracts from a contract.
/// In solidity this is the way to create a contract from a contract of the
/// same class
contract MiniMeTokenFactory {
/// @notice Update the DApp by creating a new token with new functionalities
/// the msg.sender becomes the controller of this clone token
/// @param _parentToken Address of the token being cloned
/// @param _snapshotBlock Block of the parent token that will
/// determine the initial distribution of the clone token
/// @param _tokenName Name of the new token
/// @param _decimalUnits Number of decimals of the new token
/// @param _tokenSymbol Token Symbol for the new token
/// @param _transfersEnabled If true, tokens will be able to be transferred
/// @return The address of the new token contract
function createCloneToken(
MiniMeToken _parentToken,
uint _snapshotBlock,
string calldata _tokenName,
uint8 _decimalUnits,
string calldata _tokenSymbol,
bool _transfersEnabled
) external returns (MiniMeToken)
{
MiniMeToken newToken = new MiniMeToken(
this,
_parentToken,
_snapshotBlock,
_tokenName,
_decimalUnits,
_tokenSymbol,
_transfersEnabled
);
newToken.changeController(msg.sender);
return newToken;
}
}

View File

@ -0,0 +1,218 @@
// Copied from https://github.com/OpenZeppelin/openzeppelin-solidity/blob/a9f910d34f0ab33a1ae5e714f69f9596a02b4d91/contracts/token/ERC20/StandardToken.sol
// transfer function always returns false!
pragma solidity 0.5.17;
//import "./ERC20.sol";
import "../../lib/os/ERC20.sol";
import "../../lib/os/SafeMath.sol";
/**
* @title Standard ERC20 token
*
* @dev Implementation of the basic standard token.
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
* Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
*/
contract BadTokenMock is ERC20 {
using SafeMath for uint256;
mapping (address => uint256) private balances;
mapping (address => mapping (address => uint256)) private allowed;
uint256 private totalSupply_;
constructor(address initialAccount, uint256 initialBalance) public {
balances[initialAccount] = initialBalance;
totalSupply_ = initialBalance;
}
function mint (address account, uint256 amount) public {
balances[account] = balances[account].add(amount);
totalSupply_ = totalSupply_.add(amount);
}
/**
* @dev Total number of tokens in existence
*/
function totalSupply() public view returns (uint256) {
return totalSupply_;
}
/**
* @dev Gets the balance of the specified address.
* @param _owner The address to query the the balance of.
* @return An uint256 representing the amount owned by the passed address.
*/
function balanceOf(address _owner) public view returns (uint256) {
return balances[_owner];
}
/**
* @dev Function to check the amount of tokens that an owner allowed to a spender.
* @param _owner address The address which owns the funds.
* @param _spender address The address which will spend the funds.
* @return A uint256 specifying the amount of tokens still available for the spender.
*/
function allowance(
address _owner,
address _spender
)
public
view
returns (uint256)
{
return allowed[_owner][_spender];
}
/**
* @dev Transfer token for a specified address
* @param _to The address to transfer to.
* @param _value The amount to be transferred.
*/
function transfer(address _to, uint256 _value) public returns (bool) {
require(_value <= balances[msg.sender]);
require(_to != address(0));
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
emit Transfer(msg.sender, _to, _value);
return false; // <--- Bad Token!!
}
/**
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
* Beware that changing an allowance with this method brings the risk that someone may use both the old
* and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
* race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
* @param _spender The address which will spend the funds.
* @param _value The amount of tokens to be spent.
*/
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint256 the amount of tokens to be transferred
*/
function transferFrom(
address _from,
address _to,
uint256 _value
)
public
returns (bool)
{
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);
require(_to != address(0));
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
emit Transfer(_from, _to, _value);
return true;
}
/**
* @dev Increase the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed[_spender] == 0. To increment
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _addedValue The amount of tokens to increase the allowance by.
*/
function increaseApproval(
address _spender,
uint256 _addedValue
)
public
returns (bool)
{
allowed[msg.sender][_spender] = (
allowed[msg.sender][_spender].add(_addedValue));
emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
/**
* @dev Decrease the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed[_spender] == 0. To decrement
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _subtractedValue The amount of tokens to decrease the allowance by.
*/
function decreaseApproval(
address _spender,
uint256 _subtractedValue
)
public
returns (bool)
{
uint256 oldValue = allowed[msg.sender][_spender];
if (_subtractedValue >= oldValue) {
allowed[msg.sender][_spender] = 0;
} else {
allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue);
}
emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
/**
* @dev Internal function that mints an amount of the token and assigns it to
* an account. This encapsulates the modification of balances such that the
* proper events are emitted.
* @param _account The account that will receive the created tokens.
* @param _amount The amount that will be created.
*/
function _mint(address _account, uint256 _amount) internal {
require(_account != address(0));
totalSupply_ = totalSupply_.add(_amount);
balances[_account] = balances[_account].add(_amount);
emit Transfer(address(0), _account, _amount);
}
/**
* @dev Internal function that burns an amount of the token of a given
* account.
* @param _account The account whose tokens will be burnt.
* @param _amount The amount that will be burnt.
*/
function _burn(address _account, uint256 _amount) internal {
require(_account != address(0));
require(_amount <= balances[_account]);
totalSupply_ = totalSupply_.sub(_amount);
balances[_account] = balances[_account].sub(_amount);
emit Transfer(_account, address(0), _amount);
}
/**
* @dev Internal function that burns an amount of the token of a given
* account, deducting from the sender's allowance for said account. Uses the
* internal _burn function.
* @param _account The account whose tokens will be burnt.
* @param _amount The amount that will be burnt.
*/
function _burnFrom(address _account, uint256 _amount) internal {
require(_amount <= allowed[_account][msg.sender]);
// Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted,
// this function needs to emit an event with the updated approval.
allowed[_account][msg.sender] = allowed[_account][msg.sender].sub(_amount);
_burn(_account, _amount);
}
}

View File

@ -0,0 +1,30 @@
pragma solidity 0.5.17;
import "../../lib/Checkpointing.sol";
contract CheckpointingMock {
using Checkpointing for Checkpointing.History;
Checkpointing.History history;
function add(uint64 value, uint256 time) public {
history.add(value, time);
}
function getLast() public view returns (uint256) {
return history.getLast();
}
function get(uint64 time) public view returns (uint256) {
return history.get(time);
}
function getHistorySize() public view returns (uint256) {
return history.history.length;
}
function lastUpdate() public view returns (uint256) {
return history.lastUpdate();
}
}

View File

@ -0,0 +1,26 @@
pragma solidity ^0.5.17;
/**
* @title ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/20
*/
contract ERC20 {
function totalSupply() public view returns (uint256);
function balanceOf(address _who) public view returns (uint256);
function allowance(address _owner, address _spender)
public view returns (uint256);
function transfer(address _to, uint256 _value) public returns (bool);
function approve(address _spender, uint256 _value)
public returns (bool);
function transferFrom(address _from, address _to, uint256 _value)
public returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}

View File

@ -0,0 +1,41 @@
pragma solidity 0.5.17;
import "../../locking/ILockManager.sol";
import "../../Staking.sol";
contract LockManagerMock is ILockManager {
bool result;
function slash(Staking _staking, address _from, address _to, uint256 _amount) external {
_staking.slash(_from, _to, _amount);
}
function slashAndUnstake(Staking _staking, address _from, address _to, uint256 _amount) external {
_staking.slashAndUnstake(_from, _to, _amount);
}
function unlock(Staking _staking, address _account, uint256 _amount) external {
_staking.unlock(_account, address(this), _amount);
}
function unlockAndRemoveManager(Staking _staking, address _account) external {
_staking.unlockAndRemoveManager(_account, address(this));
}
function setLockManager(Staking _staking, address _account, ILockManager _newManager) external {
_staking.setLockManager(_account, address(_newManager));
}
function canUnlock(address, uint256) external view returns (bool) {
return result;
}
function setResult(bool _result) public {
result = _result;
}
function unlockAndRemoveManager(Staking _staking, address _account, address _manager) public {
_staking.unlockAndRemoveManager(_account, _manager);
}
}

View File

@ -0,0 +1,215 @@
// Copied from https://github.com/OpenZeppelin/openzeppelin-solidity/blob/a9f910d34f0ab33a1ae5e714f69f9596a02b4d91/contracts/token/ERC20/StandardToken.sol
pragma solidity 0.5.17;
import "../../lib/os/ERC20.sol";
import "../../lib/os/SafeMath.sol";
/**
* @title Standard ERC20 token
*
* @dev Implementation of the basic standard token.
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
* Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
*/
contract NoApproveTokenMock is ERC20 {
using SafeMath for uint256;
mapping (address => uint256) private balances;
mapping (address => mapping (address => uint256)) private allowed;
uint256 private totalSupply_;
constructor(address initialAccount, uint256 initialBalance) public {
balances[initialAccount] = initialBalance;
totalSupply_ = initialBalance;
}
function mint (address account, uint256 amount) public {
balances[account] = balances[account].add(amount);
totalSupply_ = totalSupply_.add(amount);
}
/**
* @dev Total number of tokens in existence
*/
function totalSupply() public view returns (uint256) {
return totalSupply_;
}
/**
* @dev Gets the balance of the specified address.
* @param _owner The address to query the the balance of.
* @return An uint256 representing the amount owned by the passed address.
*/
function balanceOf(address _owner) public view returns (uint256) {
return balances[_owner];
}
/**
* @dev Function to check the amount of tokens that an owner allowed to a spender.
* @param _owner address The address which owns the funds.
* @param _spender address The address which will spend the funds.
* @return A uint256 specifying the amount of tokens still available for the spender.
*/
function allowance(
address _owner,
address _spender
)
public
view
returns (uint256)
{
return allowed[_owner][_spender];
}
/**
* @dev Transfer token for a specified address
* @param _to The address to transfer to.
* @param _value The amount to be transferred.
*/
function transfer(address _to, uint256 _value) public returns (bool) {
require(_value <= balances[msg.sender]);
require(_to != address(0));
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
emit Transfer(msg.sender, _to, _value);
return true;
}
/**
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
* Beware that changing an allowance with this method brings the risk that someone may use both the old
* and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
* race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
* @param _spender The address which will spend the funds.
* @param _value The amount of tokens to be spent.
*/
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint256 the amount of tokens to be transferred
*/
function transferFrom(
address _from,
address _to,
uint256 _value
)
public
returns (bool)
{
require(_value <= balances[_from]);
//require(_value <= allowed[_from][msg.sender]);
require(_to != address(0));
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
//allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
emit Transfer(_from, _to, _value);
return true;
}
/**
* @dev Increase the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed[_spender] == 0. To increment
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _addedValue The amount of tokens to increase the allowance by.
*/
function increaseApproval(
address _spender,
uint256 _addedValue
)
public
returns (bool)
{
allowed[msg.sender][_spender] = (
allowed[msg.sender][_spender].add(_addedValue));
emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
/**
* @dev Decrease the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed[_spender] == 0. To decrement
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _subtractedValue The amount of tokens to decrease the allowance by.
*/
function decreaseApproval(
address _spender,
uint256 _subtractedValue
)
public
returns (bool)
{
uint256 oldValue = allowed[msg.sender][_spender];
if (_subtractedValue >= oldValue) {
allowed[msg.sender][_spender] = 0;
} else {
allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue);
}
emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
/**
* @dev Internal function that mints an amount of the token and assigns it to
* an account. This encapsulates the modification of balances such that the
* proper events are emitted.
* @param _account The account that will receive the created tokens.
* @param _amount The amount that will be created.
*/
function _mint(address _account, uint256 _amount) internal {
require(_account != address(0));
totalSupply_ = totalSupply_.add(_amount);
balances[_account] = balances[_account].add(_amount);
emit Transfer(address(0), _account, _amount);
}
/**
* @dev Internal function that burns an amount of the token of a given
* account.
* @param _account The account whose tokens will be burnt.
* @param _amount The amount that will be burnt.
*/
function _burn(address _account, uint256 _amount) internal {
require(_account != address(0));
require(_amount <= balances[_account]);
totalSupply_ = totalSupply_.sub(_amount);
balances[_account] = balances[_account].sub(_amount);
emit Transfer(_account, address(0), _amount);
}
/**
* @dev Internal function that burns an amount of the token of a given
* account, deducting from the sender's allowance for said account. Uses the
* internal _burn function.
* @param _account The account whose tokens will be burnt.
* @param _amount The amount that will be burnt.
*/
function _burnFrom(address _account, uint256 _amount) internal {
require(_amount <= allowed[_account][msg.sender]);
// Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted,
// this function needs to emit an event with the updated approval.
allowed[_account][msg.sender] = allowed[_account][msg.sender].sub(_amount);
_burn(_account, _amount);
}
}

View File

@ -0,0 +1,54 @@
pragma solidity 0.5.17;
import "../../Staking.sol";
import "../../lib/os/SafeMath.sol";
import "./TimeHelpersMock.sol";
contract StakingMock is Staking, TimeHelpersMock {
using SafeMath for uint256;
event LogGas(uint256 gas);
string private constant ERROR_TOKEN_NOT_CONTRACT = "STAKING_TOKEN_NOT_CONTRACT";
uint64 private constant MAX_UINT64 = uint64(-1);
modifier measureGas {
uint256 initialGas = gasleft();
_;
emit LogGas(initialGas - gasleft());
}
constructor(ERC20 _stakingToken) public {
require(isContract(address(_stakingToken)), ERROR_TOKEN_NOT_CONTRACT);
initialized();
stakingToken = _stakingToken;
}
function unlockedBalanceOfGas() external returns (uint256) {
uint256 initialGas = gasleft();
_unlockedBalanceOf(msg.sender);
uint256 gasConsumed = initialGas - gasleft();
emit LogGas(gasConsumed);
return gasConsumed;
}
function transferGas(address _to, address, uint256 _amount) external measureGas {
// have enough unlocked funds
require(_amount <= _unlockedBalanceOf(msg.sender));
_transfer(msg.sender, _to, _amount);
}
function setBlockNumber(uint64 _mockedBlockNumber) public {
mockedBlockNumber = _mockedBlockNumber;
}
// Override petrify functions to allow mocking the initialization process
function petrify() internal onlyInit {
// solium-disable-previous-line no-empty-blocks
// initializedAt(PETRIFIED_BLOCK);
}
}

View File

@ -0,0 +1,215 @@
// Copied from https://github.com/OpenZeppelin/openzeppelin-solidity/blob/a9f910d34f0ab33a1ae5e714f69f9596a02b4d91/contracts/token/ERC20/StandardToken.sol
pragma solidity 0.5.17;
import "../../lib/os/ERC20.sol";
import "../../lib/os/SafeMath.sol";
/**
* @title Standard ERC20 token
*
* @dev Implementation of the basic standard token.
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
* Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
*/
contract StandardTokenMock is ERC20 {
using SafeMath for uint256;
mapping (address => uint256) private balances;
mapping (address => mapping (address => uint256)) private allowed;
uint256 private totalSupply_;
constructor(address initialAccount, uint256 initialBalance) public {
balances[initialAccount] = initialBalance;
totalSupply_ = initialBalance;
}
function mint (address account, uint256 amount) public {
balances[account] = balances[account].add(amount);
totalSupply_ = totalSupply_.add(amount);
}
/**
* @dev Total number of tokens in existence
*/
function totalSupply() public view returns (uint256) {
return totalSupply_;
}
/**
* @dev Gets the balance of the specified address.
* @param _owner The address to query the the balance of.
* @return An uint256 representing the amount owned by the passed address.
*/
function balanceOf(address _owner) public view returns (uint256) {
return balances[_owner];
}
/**
* @dev Function to check the amount of tokens that an owner allowed to a spender.
* @param _owner address The address which owns the funds.
* @param _spender address The address which will spend the funds.
* @return A uint256 specifying the amount of tokens still available for the spender.
*/
function allowance(
address _owner,
address _spender
)
public
view
returns (uint256)
{
return allowed[_owner][_spender];
}
/**
* @dev Transfer token for a specified address
* @param _to The address to transfer to.
* @param _value The amount to be transferred.
*/
function transfer(address _to, uint256 _value) public returns (bool) {
require(_value <= balances[msg.sender]);
require(_to != address(0));
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
emit Transfer(msg.sender, _to, _value);
return true;
}
/**
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
* Beware that changing an allowance with this method brings the risk that someone may use both the old
* and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
* race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
* @param _spender The address which will spend the funds.
* @param _value The amount of tokens to be spent.
*/
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint256 the amount of tokens to be transferred
*/
function transferFrom(
address _from,
address _to,
uint256 _value
)
public
returns (bool)
{
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);
require(_to != address(0));
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
emit Transfer(_from, _to, _value);
return true;
}
/**
* @dev Increase the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed[_spender] == 0. To increment
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _addedValue The amount of tokens to increase the allowance by.
*/
function increaseApproval(
address _spender,
uint256 _addedValue
)
public
returns (bool)
{
allowed[msg.sender][_spender] = (
allowed[msg.sender][_spender].add(_addedValue));
emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
/**
* @dev Decrease the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed[_spender] == 0. To decrement
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _subtractedValue The amount of tokens to decrease the allowance by.
*/
function decreaseApproval(
address _spender,
uint256 _subtractedValue
)
public
returns (bool)
{
uint256 oldValue = allowed[msg.sender][_spender];
if (_subtractedValue >= oldValue) {
allowed[msg.sender][_spender] = 0;
} else {
allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue);
}
emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
/**
* @dev Internal function that mints an amount of the token and assigns it to
* an account. This encapsulates the modification of balances such that the
* proper events are emitted.
* @param _account The account that will receive the created tokens.
* @param _amount The amount that will be created.
*/
function _mint(address _account, uint256 _amount) internal {
require(_account != address(0));
totalSupply_ = totalSupply_.add(_amount);
balances[_account] = balances[_account].add(_amount);
emit Transfer(address(0), _account, _amount);
}
/**
* @dev Internal function that burns an amount of the token of a given
* account.
* @param _account The account whose tokens will be burnt.
* @param _amount The amount that will be burnt.
*/
function _burn(address _account, uint256 _amount) internal {
require(_account != address(0));
require(_amount <= balances[_account]);
totalSupply_ = totalSupply_.sub(_amount);
balances[_account] = balances[_account].sub(_amount);
emit Transfer(_account, address(0), _amount);
}
/**
* @dev Internal function that burns an amount of the token of a given
* account, deducting from the sender's allowance for said account. Uses the
* internal _burn function.
* @param _account The account whose tokens will be burnt.
* @param _amount The amount that will be burnt.
*/
function _burnFrom(address _account, uint256 _amount) internal {
require(_amount <= allowed[_account][msg.sender]);
// Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted,
// this function needs to emit an event with the updated approval.
allowed[_account][msg.sender] = allowed[_account][msg.sender].sub(_amount);
_burn(_account, _amount);
}
}

View File

@ -0,0 +1,75 @@
pragma solidity ^0.5.17;
import "../../lib/os/TimeHelpers.sol";
import "../..//lib/os/SafeMath.sol";
import "../..//lib/os/SafeMath64.sol";
contract TimeHelpersMock is TimeHelpers {
using SafeMath for uint256;
using SafeMath64 for uint64;
uint256 public mockedTimestamp;
uint256 public mockedBlockNumber;
/**
* @dev Sets a mocked timestamp value, used only for testing purposes
*/
function mockSetTimestamp(uint256 _timestamp) external {
mockedTimestamp = _timestamp;
}
/**
* @dev Increases the mocked timestamp value, used only for testing purposes
*/
function mockIncreaseTime(uint256 _seconds) external {
if (mockedTimestamp != 0) mockedTimestamp = mockedTimestamp.add(_seconds);
else mockedTimestamp = block.timestamp.add(_seconds);
}
/**
* @dev Decreases the mocked timestamp value, used only for testing purposes
*/
function mockDecreaseTime(uint256 _seconds) external {
if (mockedTimestamp != 0) mockedTimestamp = mockedTimestamp.sub(_seconds);
else mockedTimestamp = block.timestamp.sub(_seconds);
}
/**
* @dev Advances the mocked block number value, used only for testing purposes
*/
function mockAdvanceBlocks(uint256 _number) external {
if (mockedBlockNumber != 0) mockedBlockNumber = mockedBlockNumber.add(_number);
else mockedBlockNumber = block.number.add(_number);
}
/**
* @dev Returns the mocked timestamp value
*/
function getTimestampPublic() external view returns (uint64) {
return getTimestamp64();
}
/**
* @dev Returns the mocked block number value
*/
function getBlockNumberPublic() external view returns (uint256) {
return getBlockNumber();
}
/**
* @dev Returns the mocked timestamp if it was set, or current `block.timestamp`
*/
function getTimestamp() internal view returns (uint256) {
if (mockedTimestamp != 0) return mockedTimestamp;
return super.getTimestamp();
}
/**
* @dev Returns the mocked block number if it was set, or current `block.number`
*/
function getBlockNumber() internal view returns (uint256) {
if (mockedBlockNumber != 0) return mockedBlockNumber;
return super.getBlockNumber();
}
}

View File

@ -0,0 +1,36 @@
pragma solidity 0.5.17;
import "../../locking/TimeLockManager.sol";
import "../../Staking.sol";
contract TimeLockManagerMock is TimeLockManager {
uint64 public constant MAX_UINT64 = uint64(-1);
uint256 _mockTime = now;
uint256 _mockBlockNumber = block.number;
function getTimestampExt() external view returns (uint256) {
return getTimestamp();
}
function getBlockNumberExt() external view returns (uint256) {
return getBlockNumber();
}
function setTimestamp(uint256 i) public {
_mockTime = i;
}
function setBlockNumber(uint256 i) public {
_mockBlockNumber = i;
}
function getTimestamp() internal view returns (uint256) {
return _mockTime;
}
function getBlockNumber() internal view returns (uint256) {
return _mockBlockNumber;
}
}

View File

@ -0,0 +1,18 @@
{
"name": "staking",
"version": "1.0.0",
"author": "Aragon Association <contact@aragon.org>",
"license": "GPL-3.0-or-later",
"scripts": {
"test-ganache": "yarn truffle test",
"test-ethermint": "yarn truffle test --network ethermint"
},
"devDependencies": {
"@aragon/contract-helpers-test": "^0.0.3",
"chai": "^4.2.0",
"ganache-cli": "^6.1.0",
"truffle": "^5.1.42",
"web3-eth-abi": "^1.2.11",
"web3-utils": "^1.2.11"
}
}

View File

@ -0,0 +1,52 @@
const { assertRevert } = require('@aragon/contract-helpers-test/assertThrow')
const { bn, assertBn } = require('@aragon/contract-helpers-test/numbers')
const { DEFAULT_STAKE_AMOUNT, EMPTY_DATA, ZERO_ADDRESS } = require('./helpers/constants')
const { STAKING_ERRORS } = require('./helpers/errors')
const StakingMock = artifacts.require('StakingMock')
const MiniMeToken = artifacts.require('MiniMeToken')
contract('Staking app, Approve and call fallback', ([owner, user]) => {
let staking, token, stakingAddress, tokenAddress
beforeEach(async () => {
const initialAmount = DEFAULT_STAKE_AMOUNT.mul(bn(1000))
const tokenContract = await MiniMeToken.new(ZERO_ADDRESS, ZERO_ADDRESS, 0, 'Test Token', 18, 'TT', true)
token = tokenContract
tokenAddress = tokenContract.address
await token.generateTokens(user, DEFAULT_STAKE_AMOUNT)
const stakingContract = await StakingMock.new(tokenAddress)
staking = stakingContract
stakingAddress = stakingContract.address
})
it('stakes through approveAndCall', async () => {
const initialUserBalance = await token.balanceOf(user)
const initialStakingBalance = await token.balanceOf(stakingAddress)
await token.approveAndCall(stakingAddress, DEFAULT_STAKE_AMOUNT, EMPTY_DATA, { from: user })
const finalUserBalance = await token.balanceOf(user)
const finalStakingBalance = await token.balanceOf(stakingAddress)
assertBn(finalUserBalance, initialUserBalance.sub(DEFAULT_STAKE_AMOUNT), "user balance should match")
assertBn(finalStakingBalance, initialStakingBalance.add(DEFAULT_STAKE_AMOUNT), "Staking app balance should match")
assertBn(await staking.totalStakedFor(user), DEFAULT_STAKE_AMOUNT, "staked value should match")
// total stake
assertBn(await staking.totalStaked(), DEFAULT_STAKE_AMOUNT, "Total stake should match")
})
it('fails staking 0 amount through approveAndCall', async () => {
await assertRevert(token.approveAndCall(stakingAddress, 0, EMPTY_DATA, { from: user })/*, STAKING_ERRORS.ERROR_AMOUNT_ZERO*/)
})
it('fails calling approveAndCall on a different token', async () => {
const token2 = await MiniMeToken.new(ZERO_ADDRESS, ZERO_ADDRESS, 0, 'Test Token 2', 18, 'TT2', true)
await token2.generateTokens(user, DEFAULT_STAKE_AMOUNT)
await assertRevert(token2.approveAndCall(stakingAddress, 0, EMPTY_DATA, { from: user })/*, STAKING_ERRORS.ERROR_WRONG_TOKEN*/)
})
it('fails calling receiveApproval from a different account than the token', async () => {
await assertRevert(staking.receiveApproval(user, DEFAULT_STAKE_AMOUNT, tokenAddress, EMPTY_DATA)/*, STAKING_ERRORS.ERROR_TOKEN_NOT_SENDER*/)
})
})

View File

@ -0,0 +1,83 @@
const { MAX_UINT64 } = require('@aragon/contract-helpers-test/numbers')
const getEvent = (receipt, event, arg) => { return receipt.logs.filter(l => l.event == event)[0].args[arg] }
const { deploy } = require('./helpers/deploy')(artifacts)
const { DEFAULT_STAKE_AMOUNT, DEFAULT_LOCK_AMOUNT, EMPTY_DATA, ZERO_ADDRESS, ACTIVATED_LOCK } = require('./helpers/constants')
contract.skip('Staking app, gas measures', accounts => {
let staking, token, lockManager, stakingAddress, tokenAddress, lockManagerAddress
let owner, user1, user2
const approveAndStake = async (amount = DEFAULT_STAKE_AMOUNT, from = owner) => {
await token.approve(stakingAddress, amount, { from })
await staking.stake(amount, EMPTY_DATA, { from })
}
const approveStakeAndLock = async (
manager,
lockAmount = DEFAULT_LOCK_AMOUNT,
stakeAmount = DEFAULT_STAKE_AMOUNT,
from = owner
) => {
await approveAndStake(stakeAmount, from)
await staking.allowManagerAndLock(lockAmount, manager, lockAmount, ACTIVATED_LOCK, { from })
}
before(async () => {
owner = accounts[0]
user1 = accounts[1]
user2 = accounts[2]
})
beforeEach(async () => {
const deployment = await deploy(owner)
token = deployment.token
staking = deployment.staking
lockManager = deployment.lockManager
stakingAddress = staking.address
tokenAddress = token.address
lockManagerAddress = lockManager.address
})
// increases 1185 gas for each lock
it('measures unlockedBalanceOf gas', async () => {
await approveStakeAndLock(lockManagerAddress)
const r = await staking.unlockedBalanceOfGas()
const gas = getEvent(r, 'LogGas', 'gas')
console.log(`unlockedBalanceOf gas: ${gas.toNumber()}`)
})
// 110973 gas
/*
it('measures lock gas', async () => {
await approveAndStake()
const r = await staking.lockGas(DEFAULT_LOCK_AMOUNT, lockManagerAddress, ACTIVATED_LOCK, { from: owner })
const gas = getEvent(r, 'LogGas', 'gas')
console.log('lock gas:', gas.toNumber())
})
*/
// 27601 gas
it('measures transfer gas', async () => {
await approveStakeAndLock(lockManagerAddress)
const r = await staking.transferGas(owner, lockManagerAddress, DEFAULT_LOCK_AMOUNT)
const gas = getEvent(r, 'LogGas', 'gas')
console.log('transfer gas:', gas.toNumber())
})
/*
it('measures unlock gas', async () => {
await approveStakeAndLock(user1)
const r = await staking.unlockGas(owner, user1, { from: user1 })
const gas = getEvent(r, 'LogGas', 'gas')
console.log(`unlock gas: ${gas.toNumber()}`)
await approveStakeAndLock(lockManagerAddress)
})
*/
})

View File

@ -0,0 +1,10 @@
const { bn, bigExp } = require('@aragon/contract-helpers-test/numbers')
const DEFAULT_STAKE_AMOUNT = bigExp(120, 18)
module.exports = {
DEFAULT_STAKE_AMOUNT,
DEFAULT_LOCK_AMOUNT: DEFAULT_STAKE_AMOUNT.div(bn(3)),
EMPTY_DATA: '0x',
ZERO_ADDRESS: '0x' + '0'.repeat(40),
ACTIVATED_LOCK: '0x01'
}

View File

@ -0,0 +1,35 @@
const { bn } = require('@aragon/contract-helpers-test/numbers')
const { DEFAULT_STAKE_AMOUNT } = require('./constants')
module.exports = (artifacts) => {
const StakingFactory = artifacts.require('StakingFactory')
const Staking = artifacts.require('Staking')
const StandardTokenMock = artifacts.require('StandardTokenMock')
const LockManagerMock = artifacts.require('LockManagerMock')
const getEvent = (receipt, event, arg) => { return receipt.logs.filter(l => l.event === event)[0].args[arg] }
const deploy = async (owner, initialAmount = DEFAULT_STAKE_AMOUNT.mul(bn(1000))) => {
const token = await StandardTokenMock.new(owner, initialAmount)
const staking = await deployStaking(token)
const lockManager = await LockManagerMock.new()
return { token, staking, lockManager }
}
const deployStaking = async (token) => {
const factory = await StakingFactory.new()
const receipt = await factory.getOrCreateInstance(token.address)
const stakingAddress = getEvent(receipt, 'NewStaking', 'instance')
const staking = await Staking.at(stakingAddress)
return staking
}
return {
deploy
}
}

View File

@ -0,0 +1,35 @@
const CHECKPOINT_ERRORS = {
ERROR_VALUE_TOO_BIG: 'CHECKPOINT_VALUE_TOO_BIG',
ERROR_CANNOT_ADD_PAST_VALUE: 'CHECKPOINT_CANNOT_ADD_PAST_VALUE',
}
const STAKING_ERRORS = {
ERROR_TOKEN_NOT_CONTRACT: 'STAKING_TOKEN_NOT_CONTRACT',
ERROR_AMOUNT_ZERO: 'STAKING_AMOUNT_ZERO',
ERROR_TOKEN_TRANSFER: 'STAKING_TOKEN_TRANSFER_FAIL',
ERROR_TOKEN_DEPOSIT: 'STAKING_TOKEN_DEPOSIT_FAIL',
ERROR_TOKEN_NOT_SENDER: 'STAKING_TOKEN_NOT_SENDER',
ERROR_WRONG_TOKEN: 'STAKING_WRONG_TOKEN',
ERROR_NOT_ENOUGH_BALANCE: 'STAKING_NOT_ENOUGH_BALANCE',
ERROR_NOT_ENOUGH_ALLOWANCE: 'STAKING_NOT_ENOUGH_ALLOWANCE',
ERROR_SENDER_NOT_ALLOWED: 'STAKING_SENDER_NOT_ALLOWED',
ERROR_ALLOWANCE_ZERO: 'STAKING_ALLOWANCE_ZERO',
ERROR_LOCK_ALREADY_EXISTS: 'STAKING_LOCK_ALREADY_EXISTS',
ERROR_LOCK_DOES_NOT_EXIST: 'STAKING_LOCK_DOES_NOT_EXIST',
ERROR_NOT_ENOUGH_LOCK: 'STAKING_NOT_ENOUGH_LOCK',
ERROR_CANNOT_UNLOCK: 'STAKING_CANNOT_UNLOCK',
ERROR_CANNOT_CHANGE_ALLOWANCE: 'STAKING_CANNOT_CHANGE_ALLOWANCE',
ERROR_LOCKMANAGER_CALL_FAIL: 'STAKING_LOCKMANAGER_CALL_FAIL',
ERROR_BLOCKNUMBER_TOO_BIG: 'STAKING_BLOCKNUMBER_TOO_BIG',
}
const TIME_LOCK_MANAGER_ERRORS = {
ERROR_ALREADY_LOCKED: 'TLM_ALREADY_LOCKED',
ERROR_WRONG_INTERVAL: 'TLM_WRONG_INTERVAL',
}
module.exports = {
CHECKPOINT_ERRORS,
STAKING_ERRORS,
TIME_LOCK_MANAGER_ERRORS,
}

View File

@ -0,0 +1,283 @@
const { assertRevert } = require('@aragon/contract-helpers-test/assertThrow')
const { bn, assertBn } = require('@aragon/contract-helpers-test/numbers')
const { DEFAULT_STAKE_AMOUNT, DEFAULT_LOCK_AMOUNT, EMPTY_DATA, ZERO_ADDRESS } = require('./constants')
const { STAKING_ERRORS } = require('../helpers/errors')
module.exports = (artifacts) => {
const StandardTokenMock = artifacts.require('StandardTokenMock')
const LockManagerMock = artifacts.require('LockManagerMock')
const approveAndStake = async ({ staking, amount = DEFAULT_STAKE_AMOUNT, from }) => {
const token = await StandardTokenMock.at(await staking.token())
await token.approve(staking.address, amount, { from })
await staking.stake(amount, EMPTY_DATA, { from })
}
const approveStakeAndLock = async ({
staking,
manager,
allowanceAmount = DEFAULT_LOCK_AMOUNT,
lockAmount = DEFAULT_LOCK_AMOUNT,
stakeAmount = DEFAULT_STAKE_AMOUNT,
data = EMPTY_DATA,
from
}) => {
await approveAndStake({ staking, stake: stakeAmount, from })
const receipt = await staking.allowManagerAndLock(lockAmount, manager, allowanceAmount, data, { from })
return receipt
}
// funds flows helpers
function UserState(address, walletBalance) {
this.address = address
this.walletBalance = walletBalance
this.stakedBalance = bn(0)
this.lockedBalance = bn(0)
this.walletAdd = (amount) => { this.walletBalance = this.walletBalance.add(amount) }
this.walletSub = (amount) => { this.walletBalance = this.walletBalance.sub(amount) }
this.stakedAdd = (amount) => { this.stakedBalance = this.stakedBalance.add(amount) }
this.stakedSub = (amount) => { this.stakedBalance = this.stakedBalance.sub(amount) }
this.lockedAdd = (amount) => { this.lockedBalance = this.lockedBalance.add(amount) }
this.lockedSub = (amount) => { this.lockedBalance = this.lockedBalance.sub(amount) }
this.totalBalance = () => this.walletBalance.add(this.stakedBalance)
}
const approveAndStakeWithState = async ({ staking, amount = DEFAULT_STAKE_AMOUNT, user }) => {
await approveAndStake({ staking, amount, from: user.address })
user.walletSub(amount)
user.stakedAdd(amount)
}
const approveStakeAndLockWithState = async ({
staking,
manager,
allowanceAmount = DEFAULT_LOCK_AMOUNT,
lockAmount = DEFAULT_LOCK_AMOUNT,
stakeAmount = DEFAULT_STAKE_AMOUNT,
data = EMPTY_DATA,
user
}) => {
await approveStakeAndLock({ staking, manager, allowanceAmount, lockAmount, stakeAmount, data, from: user.address })
user.walletSub(stakeAmount)
user.stakedAdd(stakeAmount)
user.lockedAdd(lockAmount)
}
const unstakeWithState = async ({ staking, unstakeAmount, user }) => {
await staking.unstake(unstakeAmount, EMPTY_DATA, { from: user.address })
user.walletAdd(unstakeAmount)
user.stakedSub(unstakeAmount)
}
const unlockWithState = async ({ staking, managerAddress, unlockAmount, user }) => {
await staking.unlock(user.address, managerAddress, unlockAmount, { from: user.address })
user.lockedSub(unlockAmount)
}
const unlockFromManagerWithState = async ({ staking, lockManager, unlockAmount, user }) => {
await lockManager.unlock(staking.address, user.address, unlockAmount, { from: user.address })
user.lockedSub(unlockAmount)
}
const transferWithState = async ({ staking, transferAmount, userFrom, userTo }) => {
await staking.transfer(userTo.address, transferAmount, { from: userFrom.address })
userTo.stakedAdd(transferAmount)
userFrom.stakedSub(transferAmount)
}
const transferAndUnstakeWithState = async ({ staking, transferAmount, userFrom, userTo }) => {
await staking.transferAndUnstake(userTo.address, transferAmount, { from: userFrom.address })
userTo.walletAdd(transferAmount)
userFrom.stakedSub(transferAmount)
}
const slashWithState = async ({ staking, slashAmount, userFrom, userTo, managerAddress }) => {
await staking.slash(userFrom.address, userTo.address, slashAmount, { from: managerAddress })
userTo.stakedAdd(slashAmount)
userFrom.stakedSub(slashAmount)
userFrom.lockedSub(slashAmount)
}
const slashAndUnstakeWithState = async ({ staking, slashAmount, userFrom, userTo, managerAddress }) => {
await staking.slashAndUnstake(userFrom.address, userTo.address, slashAmount, { from: managerAddress })
userTo.walletAdd(slashAmount)
userFrom.stakedSub(slashAmount)
userFrom.lockedSub(slashAmount)
}
const slashFromContractWithState = async ({ staking, slashAmount, userFrom, userTo, lockManager }) => {
await lockManager.slash(staking.address, userFrom.address, userTo.address, slashAmount)
userTo.stakedAdd(slashAmount)
userFrom.stakedSub(slashAmount)
userFrom.lockedSub(slashAmount)
}
const slashAndUnstakeFromContractWithState = async ({ staking, slashAmount, userFrom, userTo, lockManager }) => {
await lockManager.slashAndUnstake(staking.address, userFrom.address, userTo.address, slashAmount)
userTo.walletAdd(slashAmount)
userFrom.stakedSub(slashAmount)
userFrom.lockedSub(slashAmount)
}
// check that real user balances (token in external wallet, staked and locked) match with accounted in state
const checkUserBalances = async ({ staking, users }) => {
const token = await StandardTokenMock.at(await staking.token())
await Promise.all(
users.map(async (user) => {
assertBn(user.walletBalance, await token.balanceOf(user.address), 'token balance doesnt match')
const balances = await staking.getBalancesOf(user.address)
assertBn(user.stakedBalance, balances.staked, 'staked balance doesnt match')
assertBn(user.lockedBalance, balances.locked, 'locked balance doesnt match')
})
)
}
// check that Staking contract total staked matches with:
// - total staked by users in state (must go in combination with checkUserBalances, to make sure this is legit)
// - token balance of staking app
const checkTotalStaked = async ({ staking, users }) => {
const totalStaked = await staking.totalStaked()
const totalStakedState = users.reduce((total, user) => total.add(user.stakedBalance), bn(0))
assertBn(totalStaked, totalStakedState, 'total staked doesnt match')
const token = await StandardTokenMock.at(await staking.token())
const stakingTokenBalance = await token.balanceOf(staking.address)
assertBn(totalStaked, stakingTokenBalance, 'Staking token balance doesnt match')
}
// check that staked balance is greater than locked balance for all users
// uses local state for efficiency, so it must go with checkUserBalances
const checkStakeAndLock = ({ staking, users }) => {
users.map(user => assert.isTrue(user.stakedBalance.gte(user.lockedBalance)))
}
// check that allowed balance is always greater than locked balance, for all pairs of owner-manager
const checkAllowanceAndLock = async ({ staking, users, managers }) => {
await Promise.all(
users.map(async (user) => await Promise.all(
managers.map(async (manager) => {
const lock = await staking.getLock(user.address, manager)
assert.isTrue(lock._amount.lte(lock._allowance))
})
))
)
}
// check that users cant unstake more than unlocked balance
const checkOverUnstaking = async ({ staking, users }) => {
await Promise.all(
users.map(async (user) => {
await assertRevert(
staking.unstake(user.stakedBalance.sub(user.lockedBalance).add(bn(1)), EMPTY_DATA, { from: user.address })/*,
STAKING_ERRORS.ERROR_NOT_ENOUGH_BALANCE
*/
)
})
)
}
// check that users cant unlock more than locked balance
const checkOverUnlocking = async ({ staking, users, managers }) => {
await Promise.all(
users.map(async (user) => await Promise.all(
managers.map(async (manager) => {
const lock = await staking.getLock(user.address, manager)
// const errorMessage = lock._allowance.gt(bn(0)) ? STAKING_ERRORS.ERROR_NOT_ENOUGH_LOCK : STAKING_ERRORS.ERROR_LOCK_DOES_NOT_EXIST
await assertRevert(
staking.unlock(user.address, manager, user.lockedBalance.add(bn(1)), { from: user.address })/*,
errorMessage
*/
)
})
))
)
}
// check that users cant transfer more than unlocked balance
const checkOverTransferring = async ({ staking, users }) => {
await Promise.all(
users.map(async (user) => {
const to = user.address === users[0].address ? users[1].address : users[0].address
await assertRevert(
staking.transfer(to, user.stakedBalance.sub(user.lockedBalance).add(bn(1)), { from: user.address })/*,
STAKING_ERRORS.ERROR_NOT_ENOUGH_BALANCE
*/
)
await assertRevert(
staking.transferAndUnstake(to, user.stakedBalance.sub(user.lockedBalance).add(bn(1)), { from: user.address })/*,
STAKING_ERRORS.ERROR_NOT_ENOUGH_BALANCE
*/
)
})
)
}
// check that managers cant slash more than locked balance
const checkOverSlashing = async ({ staking, users, managers }) => {
await Promise.all(
users.map(async (user) => {
const to = user.address === users[0].address ? users[1].address : users[0].address
for (let i = 0; i < managers.length - 1; i++) {
await assertRevert(
staking.slash(user.address, to, user.lockedBalance.add(bn(1)), { from: managers[i] })/*,
STAKING_ERRORS.ERROR_NOT_ENOUGH_LOCK
*/
)
await assertRevert(
staking.slashAndUnstake(user.address, to, user.lockedBalance.add(bn(1)), { from: managers[i] }),/*
STAKING_ERRORS.ERROR_NOT_ENOUGH_LOCK
*/
)
}
// last in the array is a contract
const lockManagerAddress = managers[managers.length - 1]
const lockManager = await LockManagerMock.at(lockManagerAddress)
await assertRevert(
lockManager.slash(staking.address, user.address, to, user.lockedBalance.add(bn(1))),/*
STAKING_ERRORS.ERROR_NOT_ENOUGH_LOCK
*/
)
await assertRevert(
lockManager.slashAndUnstake(staking.address, user.address, to, user.lockedBalance.add(bn(1))),/*
STAKING_ERRORS.ERROR_NOT_ENOUGH_LOCK
*/
)
})
)
}
const checkInvariants = async ({ staking, users, managers }) => {
await checkUserBalances({ staking, users })
await checkTotalStaked({ staking, users })
checkStakeAndLock({ staking, users })
await checkAllowanceAndLock({ staking, users, managers })
await checkOverUnstaking({ staking, users })
await checkOverUnlocking({ staking, users, managers })
await checkOverTransferring({ staking, users })
await checkOverSlashing({ staking, users, managers })
}
return {
approveAndStake,
approveStakeAndLock,
UserState,
approveAndStakeWithState,
approveStakeAndLockWithState,
unstakeWithState,
unlockWithState,
unlockFromManagerWithState,
transferWithState,
transferAndUnstakeWithState,
slashWithState,
slashAndUnstakeWithState,
slashFromContractWithState,
slashAndUnstakeFromContractWithState,
checkInvariants,
}
}

View File

@ -0,0 +1,226 @@
const { assertRevert } = require('@aragon/contract-helpers-test/assertThrow')
const { bn, assertBn, MAX_UINT256 } = require('@aragon/contract-helpers-test/numbers')
const { CHECKPOINT_ERRORS } = require('../helpers/errors')
const Checkpointing = artifacts.require('CheckpointingMock')
contract('Checkpointing', () => {
let checkpointing
beforeEach('create tree', async () => {
checkpointing = await Checkpointing.new()
})
const assertFetchedValue = async (time, expectedValue) => {
assertBn((await checkpointing.get(time)), expectedValue, 'value does not match')
}
describe('add', () => {
context('when the given value is can be represented by 192 bits', () => {
const value = bn(100)
context('when there was no value registered yet', async () => {
context('when the given time is zero', async () => {
const time = bn(0)
it('adds the new value', async () => {
await checkpointing.add(time, value)
await assertFetchedValue(time, value)
})
})
context('when the given time is greater than zero', async () => {
const time= bn(1)
it('adds the new value', async () => {
await checkpointing.add(time, value)
await assertFetchedValue(time, value)
})
})
})
context('when there were some values already registered', async () => {
beforeEach('add some values', async () => {
await checkpointing.add(30, 1)
await checkpointing.add(50, 2)
await checkpointing.add(90, 3)
})
context('when the given time is previous to the latest registered value', async () => {
const time= bn(40)
it('reverts', async () => {
await assertRevert(checkpointing.add(time, value)/*, CHECKPOINT_ERRORS.CANNOT_ADD_PAST_VALUE*/)
})
})
context('when the given time is equal to the latest registered value', async () => {
const time= bn(90)
it('updates the already registered value', async () => {
await checkpointing.add(time, value)
await assertFetchedValue(time, value)
await assertFetchedValue(time.add(bn(1)), value)
})
})
context('when the given time is after the latest registered value', async () => {
const time= bn(95)
it('adds the new last value', async () => {
const previousLast = await checkpointing.getLast()
await checkpointing.add(time, value)
await assertFetchedValue(time, value)
await assertFetchedValue(time.add(bn(1)), value)
await assertFetchedValue(time.sub(bn(1)), previousLast)
})
})
})
})
context('when the given value cannot be represented by 192 bits', () => {
const value = MAX_UINT256
it('reverts', async () => {
await assertRevert(checkpointing.add(0, value)/*, CHECKPOINT_ERRORS.VALUE_TOO_BIG*/)
})
})
})
describe('lastUpdate', () => {
context('when there are no values registered yet', () => {
it('returns zero', async () => {
assertBn((await checkpointing.lastUpdate()), bn(0), 'time does not match')
})
})
context('when there are values already registered', () => {
beforeEach('add some values', async () => {
await checkpointing.add(30, 1)
await checkpointing.add(50, 2)
await checkpointing.add(90, 3)
})
it('returns the last registered value', async () => {
assertBn((await checkpointing.lastUpdate()), bn(90), 'time does not match')
})
})
})
describe('getLast', () => {
context('when there are no values registered yet', () => {
it('returns zero', async () => {
assertBn((await checkpointing.getLast()), bn(0), 'value does not match')
})
})
context('when there are values already registered', () => {
beforeEach('add some values', async () => {
await checkpointing.add(30, 1)
await checkpointing.add(50, 2)
await checkpointing.add(90, 3)
})
it('returns the last registered value', async () => {
assertBn((await checkpointing.getLast()), bn(3), 'value does not match')
})
})
})
describe('get', () => {
context('when there are no values registered yet', () => {
context('when there given time is zero', () => {
const time= bn(0)
it('returns zero', async () => {
await assertFetchedValue(time, bn(0))
})
})
context('when there given time is greater than zero', () => {
const time= bn(1)
it('returns zero', async () => {
await assertFetchedValue(time, bn(0))
})
})
})
context('when there are values already registered', () => {
beforeEach('add some values', async () => {
await checkpointing.add(30, 1)
await checkpointing.add(50, 2)
await checkpointing.add(90, 3)
})
context('when there given time is zero', () => {
const time= bn(0)
it('returns zero', async () => {
await assertFetchedValue(time, bn(0))
})
})
context('when the given time is previous to the time of first registered value', () => {
const time= bn(10)
it('returns zero', async () => {
await assertFetchedValue(time, bn(0))
})
})
context('when the given time is equal to the time of first registered value', () => {
const time= bn(30)
it('returns the first registered value', async () => {
await assertFetchedValue(time, bn(1))
})
})
context('when the given time is between the times of first and the second registered values', () => {
const time= bn(40)
it('returns the first registered value', async () => {
await assertFetchedValue(time, bn(1))
})
})
context('when the given time is the time of the second registered values', () => {
const time= bn(50)
it('returns the second registered value', async () => {
await assertFetchedValue(time, bn(2))
})
})
context('when the given time is between the times of second and the third registered values', () => {
const time= bn(60)
it('returns the second registered value', async () => {
await assertFetchedValue(time, bn(2))
})
})
context('when the given time is equal to the time of the third registered values', () => {
const time= bn(90)
it('returns the third registered value', async () => {
await assertFetchedValue(time, bn(3))
})
})
context('when the given time is after the time of the third registered values', () => {
const time= bn(100)
it('returns the third registered value', async () => {
await assertFetchedValue(time, bn(3))
})
})
})
})
})

View File

@ -0,0 +1,308 @@
const { assertRevert } = require('@aragon/contract-helpers-test/assertThrow')
const { bn, assertBn, MAX_UINT64 } = require('@aragon/contract-helpers-test/numbers')
const { deploy } = require('../helpers/deploy')(artifacts)
const {
UserState,
approveAndStakeWithState,
approveStakeAndLockWithState,
unstakeWithState,
unlockWithState,
unlockFromManagerWithState,
transferWithState,
transferAndUnstakeWithState,
slashWithState,
slashAndUnstakeWithState,
slashFromContractWithState,
slashAndUnstakeFromContractWithState,
checkInvariants,
} = require('../helpers/helpers')(artifacts)
const { DEFAULT_STAKE_AMOUNT, DEFAULT_LOCK_AMOUNT, EMPTY_DATA, ZERO_ADDRESS } = require('../helpers/constants')
contract('Staking app, Locking funds flows', ([_, owner, user1, user2, user3]) => {
let staking, lockManager, users, managers, token
beforeEach(async () => {
const deployment = await deploy(owner)
staking = deployment.staking
lockManager = deployment.lockManager
token = deployment.token
// fund users and create user state objects
users = []
const userAddresses = [user1, user2, user3]
await Promise.all(userAddresses.map(async (userAddress, index) => {
const amount = DEFAULT_STAKE_AMOUNT.mul(bn(userAddresses.length - index))
users.push(new UserState(userAddress, amount))
await token.transfer(userAddress, amount, { from: owner })
}))
// managers
managers = users.reduce((result, user) => {
result.push(user.address)
return result
}, [])
managers.push(lockManager.address)
})
describe('same origin and destiny', () => {
context('when user hasnt staked', () => {
it('check invariants', async () => {
await checkInvariants({ staking, users, managers })
})
})
context('when user has staked', () => {
const stakeAmount = DEFAULT_STAKE_AMOUNT
beforeEach('stakes', async () => {
await checkInvariants({ staking, users, managers })
await approveAndStakeWithState({ staking, amount: stakeAmount, user: users[0] })
await checkInvariants({ staking, users, managers })
})
context('when user hasnt locked', () => {
it('unstakes half', async () => {
await unstakeWithState({ staking, unstakeAmount: stakeAmount.div(bn(2)), user: users[0] })
await checkInvariants({ staking, users, managers })
})
it('unstakes all', async () => {
await unstakeWithState({ staking, unstakeAmount: stakeAmount, user: users[0] })
await checkInvariants({ staking, users, managers })
})
})
context('when user has locked', () => {
const lockAmount = DEFAULT_LOCK_AMOUNT
const moveFunds = ({ isContract, canUnlock = false }) => {
let lockManagerAddress
beforeEach('stakes and locks', async () => {
lockManagerAddress = isContract ? lockManager.address : user3
if (isContract && canUnlock) {
await lockManager.setResult(true)
}
await checkInvariants({ staking, users, managers })
await approveStakeAndLockWithState({
staking,
manager: lockManagerAddress,
stakeAmount,
allowanceAmount: stakeAmount,
lockAmount,
user: users[0]
})
await checkInvariants({ staking, users, managers })
})
const unstake = async (unstakeAmount) => {
await unstakeWithState({ staking, unstakeAmount, user: users[0] })
await checkInvariants({ staking, users, managers })
}
it('unstakes remaining', async () => {
await unstake(stakeAmount.sub(lockAmount))
})
const unlockAndUnstake = async (unlockAmount) => {
await unlockWithState({ staking, managerAddress: lockManagerAddress, unlockAmount, user: users[0]})
await unstake(stakeAmount.sub(lockAmount.sub(unlockAmount)))
}
const unlockAndUnstakeFromManager = async (unlockAmount) => {
await unlockFromManagerWithState({ staking, lockManager, unlockAmount, user: users[0]})
await unstake(stakeAmount.sub(lockAmount.sub(unlockAmount)))
}
if (canUnlock) {
it('owner unlocks half and then unstakes', async () => {
await unlockAndUnstake(lockAmount.div(bn(2)))
})
it('owner unlocks all and then unstakes', async () => {
await unlockAndUnstake(lockAmount)
})
} else {
it('owner cannot unlock', async () => {
await assertRevert(staking.unlock(users[0].address, lockManagerAddress, bn(1), { from: users[0].address }))
})
if (isContract) {
it('manager unlocks half and then owner unstakes', async () => {
await unlockAndUnstakeFromManager(lockAmount.div(bn(2)))
})
it('manager unlocks all and then owner unstakes', async () => {
await unlockAndUnstakeFromManager(lockAmount)
})
}
}
}
context('when lock manager is EOA', () => {
moveFunds({ isContract: false, canUnlock: false })
})
context('when lock manager is contract', () => {
context('when lock manager allows to unlock', () => {
moveFunds({ isContract: true, canUnlock: true })
})
context('when lock manager doesnt allow to unlock', () => {
moveFunds({ isContract: true, canUnlock: false })
})
})
})
})
})
describe('different origin and destiny', () => {
context('when user hasnt staked', () => {
it('check invariants', async () => {
await checkInvariants({ staking, users, managers })
})
})
context('when user has staked', () => {
const stakeAmount = DEFAULT_STAKE_AMOUNT
beforeEach('stakes', async () => {
await checkInvariants({ staking, users, managers })
await approveAndStakeWithState({ staking, amount: stakeAmount, user: users[0] })
await checkInvariants({ staking, users, managers })
})
context('when user hasnt locked', () => {
context('to staking balance', () => {
it('transfers half', async () => {
await transferWithState({ staking, transferAmount: stakeAmount.div(bn(2)), userFrom: users[0], userTo: users[1] })
await checkInvariants({ staking, users, managers })
})
it('transfers all', async () => {
await transferWithState({ staking, transferAmount: stakeAmount, userFrom: users[0], userTo: users[1] })
await checkInvariants({ staking, users, managers })
})
})
context('to external wallet', () => {
it('transfers half', async () => {
await transferAndUnstakeWithState({ staking, transferAmount: stakeAmount.div(bn(2)), userFrom: users[0], userTo: users[1] })
await checkInvariants({ staking, users, managers })
})
it('transfers all', async () => {
await transferAndUnstakeWithState({ staking, transferAmount: stakeAmount, userFrom: users[0], userTo: users[1] })
await checkInvariants({ staking, users, managers })
})
})
})
context('when user has locked', () => {
const lockAmount = DEFAULT_LOCK_AMOUNT
const moveFunds = ({ isContract, canUnlock = false, toStaking }) => {
let lockManagerAddress
beforeEach('stakes and locks', async () => {
lockManagerAddress = isContract ? lockManager.address : user3
if (isContract && canUnlock) {
await lockManager.setResult(true)
}
await checkInvariants({ staking, users, managers })
await approveStakeAndLockWithState({
staking,
manager: lockManagerAddress,
stakeAmount,
allowanceAmount: stakeAmount,
lockAmount,
user: users[0]
})
await checkInvariants({ staking, users, managers })
})
const transfer = async (transferAmount) => {
await transferWithState({ staking, transferAmount, userFrom: users[0], userTo: users[1] })
await checkInvariants({ staking, users, managers })
}
it('transfers remaining', async () => {
await transfer(stakeAmount.sub(lockAmount))
})
const slashAndTransfer = async (slashAmount) => {
if (toStaking) {
await slashWithState({ staking, slashAmount, userFrom: users[0], userTo: users[1], managerAddress: lockManagerAddress })
} else {
await slashAndUnstakeWithState({ staking, slashAmount, userFrom: users[0], userTo: users[1], managerAddress: lockManagerAddress })
}
await transfer(stakeAmount.sub(lockAmount.sub(slashAmount)))
}
const slashAndTransferFromContract = async (slashAmount) => {
if (toStaking) {
await slashFromContractWithState({ staking, slashAmount, userFrom: users[0], userTo: users[1], lockManager })
} else {
await slashAndUnstakeFromContractWithState({ staking, slashAmount, userFrom: users[0], userTo: users[1], lockManager })
}
await transfer(stakeAmount.sub(lockAmount.sub(slashAmount)))
}
if (isContract) {
it('manager unlockes half and then owner transfers', async () => {
await slashAndTransferFromContract(lockAmount.div(bn(2)))
})
it('manager slashes all and then owner transfers', async () => {
await slashAndTransferFromContract(lockAmount)
})
} else {
it('manager slashes half and then transfers', async () => {
await slashAndTransfer(lockAmount.div(bn(2)))
})
it('manager slashes all and then transfers', async () => {
await slashAndTransfer(lockAmount)
})
}
}
context('when lock manager is EOA', () => {
context('to staking balance', () => {
moveFunds({ isContract: false, canUnlock: false, toStaking: true })
})
context('to external wallet', () => {
moveFunds({ isContract: false, canUnlock: false, toStaking: false })
})
})
context('when lock manager is contract', () => {
context('when lock manager allows to unlock', () => {
context('to staking balance', () => {
moveFunds({ isContract: true, canUnlock: true, toStaking: true })
})
context('to external wallet', () => {
moveFunds({ isContract: true, canUnlock: true, toStaking: false })
})
})
context('when lock manager doesnt allow to unlock', () => {
context('to staking balance', () => {
moveFunds({ isContract: true, canUnlock: false, toStaking: true })
})
context('to external wallet', () => {
moveFunds({ isContract: true, canUnlock: false, toStaking: false })
})
})
})
})
})
})
})

View File

@ -0,0 +1,395 @@
const { assertRevert } = require('@aragon/contract-helpers-test/assertThrow')
const { bn, bigExp, assertBn, MAX_UINT64 } = require('@aragon/contract-helpers-test/numbers')
const { deploy } = require('../helpers/deploy')(artifacts)
const { approveAndStake, approveStakeAndLock } = require('../helpers/helpers')(artifacts)
const { DEFAULT_STAKE_AMOUNT, DEFAULT_LOCK_AMOUNT, EMPTY_DATA, ZERO_ADDRESS } = require('../helpers/constants')
const { STAKING_ERRORS } = require('../helpers/errors')
contract('Staking app, Locking', ([owner, user1, user2]) => {
let staking, lockManager
beforeEach(async () => {
const deployment = await deploy(owner)
staking = deployment.staking
lockManager = deployment.lockManager
})
it('allows new manager and locks amount', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
// check lock values
const { _amount, _allowance } = await staking.getLock(owner, user1)
assertBn(_amount, DEFAULT_LOCK_AMOUNT, "locked amount should match")
assertBn(_allowance, DEFAULT_LOCK_AMOUNT, "locked allowance should match")
assertBn(await staking.unlockedBalanceOf(owner), DEFAULT_STAKE_AMOUNT.sub(DEFAULT_LOCK_AMOUNT), "Unlocked balance should match")
const { staked, locked } = await staking.getBalancesOf(owner)
assertBn(staked, DEFAULT_STAKE_AMOUNT, "Staked balance should match")
assertBn(locked, DEFAULT_LOCK_AMOUNT, "Locked balance should match")
})
it('fails locking 0 tokens', async () => {
await approveAndStake({ staking, from: owner })
await assertRevert(staking.allowManagerAndLock(0, user1, 1, EMPTY_DATA), STAKING_ERRORS.ERROR_AMOUNT_ZERO)
})
it('fails locking without enough allowance', async () => {
await approveAndStake({ staking, from: owner })
await assertRevert(staking.allowManagerAndLock(2, user1, 1, EMPTY_DATA), STAKING_ERRORS.ERROR_NOT_ENOUGH_ALLOWANCE)
})
it('fails locking more tokens than staked', async () => {
await approveAndStake({ staking, from: owner })
await assertRevert(staking.allowManagerAndLock(DEFAULT_STAKE_AMOUNT.add(bn(1)), user1, DEFAULT_STAKE_AMOUNT.add(bn(1)), EMPTY_DATA), STAKING_ERRORS.ERROR_NOT_ENOUGH_BALANCE)
})
it('fails locking if already locked', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
await approveAndStake({ staking, from: owner })
await assertRevert(staking.allowManagerAndLock(DEFAULT_STAKE_AMOUNT, user1, DEFAULT_STAKE_AMOUNT, "0x02"), STAKING_ERRORS.ERROR_LOCK_ALREADY_EXISTS)
})
it('fails unstaking locked tokens', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
await assertRevert(staking.unstake(DEFAULT_STAKE_AMOUNT, EMPTY_DATA), STAKING_ERRORS.ERROR_NOT_ENOUGH_BALANCE)
})
it('creates a new allowance', async () => {
await staking.allowManager(user1, DEFAULT_LOCK_AMOUNT, EMPTY_DATA)
const { _allowance } = await staking.getLock(owner, user1)
assertBn(_allowance, DEFAULT_LOCK_AMOUNT, "allowed amount should match")
})
it('creates a new allowance and then lock manager locks', async () => {
await approveAndStake({ staking, from: owner })
await staking.allowManager(user1, DEFAULT_LOCK_AMOUNT, EMPTY_DATA)
await staking.lock(owner, user1, DEFAULT_LOCK_AMOUNT, { from: user1 })
// check lock values
const { _amount, _allowance } = await staking.getLock(owner, user1)
assertBn(_amount, DEFAULT_LOCK_AMOUNT, "locked amount should match")
assertBn(_allowance, DEFAULT_LOCK_AMOUNT, "locked allowance should match")
assertBn(await staking.unlockedBalanceOf(owner), DEFAULT_STAKE_AMOUNT.sub(DEFAULT_LOCK_AMOUNT), "Unlocked balance should match")
})
it('fails creating allowance of 0 tokens', async () => {
await assertRevert(staking.allowManager(user1, 0, EMPTY_DATA), STAKING_ERRORS.ERROR_AMOUNT_ZERO)
})
it('fails creating allowance if lock exists', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
await assertRevert(staking.allowManager(user1, 1, EMPTY_DATA), STAKING_ERRORS.ERROR_LOCK_ALREADY_EXISTS)
})
it('increases allowance of existing lock', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
await staking.increaseLockAllowance(user1, DEFAULT_LOCK_AMOUNT)
const { _allowance } = await staking.getLock(owner, user1)
assertBn(_allowance, DEFAULT_LOCK_AMOUNT.mul(bn(2)), "allowed amount should match")
})
it('fails increasing allowance of non-existing', async () => {
await assertRevert(staking.increaseLockAllowance(user1, 1), STAKING_ERRORS.ERROR_LOCK_DOES_NOT_EXIST)
})
it('fails increasing allowance of existing lock by 0', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
await assertRevert(staking.increaseLockAllowance(user1, 0), STAKING_ERRORS.ERROR_AMOUNT_ZERO)
})
it('fails increasing allowance of existing lock if not owner or manager', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
await assertRevert(staking.increaseLockAllowance(user1, 1, { from: user2 }), STAKING_ERRORS.ERROR_LOCK_DOES_NOT_EXIST)
})
it('decreases allowance of existing lock by the owner', async () => {
await approveAndStake({ staking, from: owner })
await staking.allowManagerAndLock(DEFAULT_LOCK_AMOUNT, user1, DEFAULT_LOCK_AMOUNT.add(bn(1)), EMPTY_DATA)
await staking.decreaseLockAllowance(owner, user1, 1, { from: owner })
const { _allowance } = await staking.getLock(owner, user1)
assertBn(_allowance, DEFAULT_LOCK_AMOUNT, "allowed amount should match")
})
it('decreases allowance of existing lock by manager', async () => {
await approveAndStake({ staking, from: owner })
await staking.allowManagerAndLock(DEFAULT_LOCK_AMOUNT, user1, DEFAULT_LOCK_AMOUNT.add(bn(1)), EMPTY_DATA)
await staking.decreaseLockAllowance(owner, user1, 1, { from: user1 })
const { _allowance } = await staking.getLock(owner, user1)
assertBn(_allowance, DEFAULT_LOCK_AMOUNT, "allowed amount should match")
})
it('fails decreasing allowance of existing lock by 0', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
await assertRevert(staking.decreaseLockAllowance(owner, user1, 0), STAKING_ERRORS.ERROR_AMOUNT_ZERO)
})
it('fails decreasing allowance of existing lock to 0', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
await staking.unlock(owner, user1, DEFAULT_LOCK_AMOUNT, { from: user1 })
await assertRevert(staking.decreaseLockAllowance(owner, user1, DEFAULT_LOCK_AMOUNT), STAKING_ERRORS.ERROR_ALLOWANCE_ZERO)
})
it('fails decreasing allowance to less than lock', async () => {
await approveAndStake({ staking, from: owner })
await staking.allowManagerAndLock(DEFAULT_LOCK_AMOUNT, user1, DEFAULT_LOCK_AMOUNT.add(bn(1)), EMPTY_DATA)
await assertRevert(staking.decreaseLockAllowance(owner, user1, 2), STAKING_ERRORS.ERROR_NOT_ENOUGH_ALLOWANCE)
})
it('fails decreasing allowance by 3rd party', async () => {
await approveAndStake({ staking, from: owner })
await staking.allowManagerAndLock(DEFAULT_LOCK_AMOUNT, user1, DEFAULT_LOCK_AMOUNT.add(bn(1)), EMPTY_DATA)
await assertRevert(staking.decreaseLockAllowance(owner, user1, 1, { from: user2 }), STAKING_ERRORS.ERROR_CANNOT_CHANGE_ALLOWANCE)
})
it('increases amount of existing lock', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
await approveAndStake({ staking, from: owner })
await staking.increaseLockAllowance(user1, DEFAULT_LOCK_AMOUNT)
await staking.lock(owner, user1, DEFAULT_LOCK_AMOUNT)
const { _amount } = await staking.getLock(owner, user1)
assertBn(_amount, DEFAULT_LOCK_AMOUNT.mul(bn(2)), "locked amount should match")
})
it('fails increasing lock with 0 tokens', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
await approveAndStake({ staking, from: owner })
await assertRevert(staking.lock(owner, user1, 0), STAKING_ERRORS.ERROR_AMOUNT_ZERO)
})
it('fails increasing lock with more tokens than staked', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
await approveAndStake({ staking, from: owner })
await assertRevert(staking.lock(owner, user1, DEFAULT_STAKE_AMOUNT.mul(bn(2)).add(bn(1))), STAKING_ERRORS.ERROR_NOT_ENOUGH_BALANCE)
})
it('fails increasing lock if not owner or manager', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
await approveAndStake({ staking, from: owner })
await assertRevert(staking.lock(owner, user1, 1, { from: user2 }), STAKING_ERRORS.ERROR_SENDER_NOT_ALLOWED)
})
it('unlocks with only 1 lock, EOA manager', async () => {
await approveStakeAndLock({ staking, manager: user1, lockAmount: DEFAULT_LOCK_AMOUNT, from: owner })
// unlock
await staking.unlockAndRemoveManager(owner, user1, { from: user1 })
assertBn(await staking.unlockedBalanceOf(owner), DEFAULT_STAKE_AMOUNT, "Unlocked balance should match")
assertBn(await staking.lockedBalanceOf(owner), bn(0), "total locked doesnt match")
})
it('unlocks with more than 1 lock, EOA manager', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
// lock again
await staking.allowManagerAndLock(DEFAULT_LOCK_AMOUNT, user2, DEFAULT_LOCK_AMOUNT, EMPTY_DATA)
const previousTotalLocked = await staking.lockedBalanceOf(owner)
// unlock
await staking.unlockAndRemoveManager(owner, user1, { from: user1 })
assertBn(await staking.unlockedBalanceOf(owner), DEFAULT_STAKE_AMOUNT.sub(DEFAULT_LOCK_AMOUNT), "Unlocked balance should match")
assertBn(await staking.lockedBalanceOf(owner), previousTotalLocked.sub(bn(DEFAULT_LOCK_AMOUNT)), "total locked doesnt match")
})
it('unlocks completely, contract manager, called by owner', async () => {
await lockManager.setResult(true)
await approveStakeAndLock({ staking, manager: lockManager.address, from: owner })
// unlock
await staking.unlockAndRemoveManager(owner, lockManager.address, { from: owner })
assertBn(await staking.unlockedBalanceOf(owner), DEFAULT_STAKE_AMOUNT, "Unlocked balance should match")
assertBn(await staking.lockedBalanceOf(owner), bn(0), "total locked doesnt match")
})
it('unlocks completely, contract manager, called by manager', async () => {
await lockManager.setResult(true)
await approveStakeAndLock({ staking, manager: lockManager.address, from: owner })
// unlock
await lockManager.unlockAndRemoveManager(staking.address, owner, lockManager.address)
assertBn(await staking.unlockedBalanceOf(owner), DEFAULT_STAKE_AMOUNT, "Unlocked balance should match")
assertBn(await staking.lockedBalanceOf(owner), bn(0), "total locked doesnt match")
})
it('unlocks completely, contract manager, called by manager, even if condition is not satisfied', async () => {
// not needed, is false by default
//await lockManager.setResult(false)
await approveStakeAndLock({ staking, manager: lockManager.address, from: owner })
// unlock
await lockManager.unlockAndRemoveManager(staking.address, owner, lockManager.address)
assertBn(await staking.unlockedBalanceOf(owner), DEFAULT_STAKE_AMOUNT, "Unlocked balance should match")
assertBn(await staking.lockedBalanceOf(owner), bn(0), "total locked doesnt match")
})
it('fails calling canUnlock, EOA manager', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
// call canUnlock
await assertRevert(staking.canUnlock(owner, owner, user1, 0)) // no reason: its trying to call an EOA
})
it('can unlock if amount is zero', async () => {
await staking.allowManager(user1, DEFAULT_LOCK_AMOUNT, EMPTY_DATA, { from: owner })
assert.isTrue(await staking.canUnlock(owner, owner, user1, 0))
})
it('fails to unlock if it cannot unlock, EOA manager', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
// tries to unlock
await assertRevert(staking.unlockAndRemoveManager(owner, user1)) // no reason: its trying to call an EOA
})
it('fails to unlock if can not unlock, contract manager, called by owner', async () => {
// not needed, is false by default
// await lockManager.setResult(false)
await approveStakeAndLock({ staking, manager: lockManager.address, from: owner })
// tries to unlock
await assertRevert(staking.unlockAndRemoveManager(owner, lockManager.address, { from: owner }), STAKING_ERRORS.ERROR_CANNOT_UNLOCK)
})
it('fails to unlock if, contract manager, called by 3rd party (even if condition is true)', async () => {
await lockManager.setResult(true)
await approveStakeAndLock({ staking, manager: lockManager.address, from: owner })
// tries to unlock
await assertRevert(staking.unlockAndRemoveManager(owner, lockManager.address, { from: user1 }), STAKING_ERRORS.ERROR_CANNOT_UNLOCK)
})
it('transfers (slash) and unlocks (everything else) in one transaction', async () => {
const totalLock = bigExp(120, 18)
const transferAmount = bigExp(40, 18)
await approveStakeAndLock({ staking, manager: user1, allowanceAmount: totalLock, lockAmount: totalLock, stakeAmount: totalLock, from: owner })
// unlock and transfer
await staking.slashAndUnlock(owner, user2, totalLock.sub(transferAmount), transferAmount, { from: user1 })
assertBn(await staking.unlockedBalanceOf(owner), totalLock.sub(transferAmount), "Unlocked balance should match")
assertBn(await staking.lockedBalanceOf(owner), bn(0), "total locked doesnt match")
// lock manager
assertBn(await staking.unlockedBalanceOf(user1), bn(0), "Unlocked balance should match")
assertBn(await staking.lockedBalanceOf(user1), bn(0), "total locked doesnt match")
// recipient
assertBn(await staking.unlockedBalanceOf(user2), transferAmount, "Unlocked balance should match")
assertBn(await staking.lockedBalanceOf(user2), bn(0), "total locked doesnt match")
})
it('transfers (slash) and unlocks in one transaction', async () => {
const totalLock = bigExp(120, 18)
const transferAmount = bigExp(40, 18)
const decreaseAmount = bigExp(60, 18)
await approveStakeAndLock({ staking, manager: user1, allowanceAmount: totalLock, lockAmount: totalLock, stakeAmount: totalLock, from: owner })
// unlock and transfer
await staking.slashAndUnlock(owner, user2, decreaseAmount, transferAmount, { from: user1 })
assertBn(await staking.unlockedBalanceOf(owner), decreaseAmount, "Unlocked balance should match")
assertBn(await staking.lockedBalanceOf(owner), totalLock.sub(decreaseAmount).sub(transferAmount), "total locked doesnt match")
// lock manager
assertBn(await staking.unlockedBalanceOf(user1), bn(0), "Unlocked balance should match")
assertBn(await staking.lockedBalanceOf(user1), bn(0), "total locked doesnt match")
// recipient
assertBn(await staking.unlockedBalanceOf(user2), transferAmount, "Unlocked balance should match")
assertBn(await staking.lockedBalanceOf(user2), bn(0), "total locked doesnt match")
})
it('fails to transfer (slash) and unlocks in one transaction if unlock amount is zero', async () => {
const totalLock = bigExp(120, 18)
const transferAmount = bigExp(40, 18)
const decreaseAmount = bigExp(0, 18)
await approveStakeAndLock({ staking, manager: user1, allowanceAmount: totalLock, lockAmount: totalLock, stakeAmount: totalLock, from: owner })
// unlock and transfer
await assertRevert(staking.slashAndUnlock(owner, user2, decreaseAmount, transferAmount, { from: user1 }), STAKING_ERRORS.ERROR_AMOUNT_ZERO)
})
it('fails to transfer (slash) and unlock in one transaction if not owner nor manager', async () => {
const totalLock = bigExp(120, 18)
const transferAmount = bigExp(40, 18)
const decreaseAmount = bigExp(60, 18)
await approveStakeAndLock({ staking, manager: user1, allowanceAmount: totalLock, lockAmount: totalLock, stakeAmount: totalLock, from: owner })
// unlock and transfer
await assertRevert(staking.slashAndUnlock(owner, user2, decreaseAmount, transferAmount, { from: user2 }), STAKING_ERRORS.ERROR_NOT_ENOUGH_LOCK)
})
it('change lock amount', async () => {
await approveStakeAndLock({ staking, manager: lockManager.address, from: owner })
const { _amount: amount1 } = await staking.getLock(owner, lockManager.address)
assertBn(amount1, bn(DEFAULT_LOCK_AMOUNT), "Amount should match")
assertBn(await staking.unlockedBalanceOf(owner), DEFAULT_STAKE_AMOUNT.sub(DEFAULT_LOCK_AMOUNT), "Unlocked balance should match")
// change amount
const unlockAmount = DEFAULT_LOCK_AMOUNT.div(bn(2))
await lockManager.unlock(staking.address, owner, unlockAmount)
const { _amount: amount2 } = await staking.getLock(owner, lockManager.address)
assertBn(amount2, unlockAmount, "Amount should match")
assertBn(await staking.unlockedBalanceOf(owner), DEFAULT_STAKE_AMOUNT.sub(unlockAmount), "Unlocked balance should match")
})
it('fails to change lock amount to zero', async () => {
await approveStakeAndLock({ staking, manager: lockManager.address, from: owner })
// try to change amount
await assertRevert(lockManager.unlock(staking.address, owner, 0), STAKING_ERRORS.ERROR_AMOUNT_ZERO)
})
it('fails to change lock amount to greater than before', async () => {
await approveStakeAndLock({ staking, manager: lockManager.address, from: owner })
// try to change amount
await assertRevert(lockManager.unlock(staking.address, owner, DEFAULT_LOCK_AMOUNT.add(bn(1))), STAKING_ERRORS.ERROR_NOT_ENOUGH_LOCK)
})
it('change lock manager', async () => {
await approveStakeAndLock({ staking, manager: user1, from: owner })
assert.equal(await staking.canUnlock(user1, owner, user1, 0), true, "User 1 can unlock")
assert.equal(await staking.canUnlock(user2, owner, user1, 0), false, "User 2 can not unlock")
await assertRevert(staking.canUnlock(user2, owner, user2, 0), STAKING_ERRORS.ERROR_LOCK_DOES_NOT_EXIST) // it doesnt exist
// change manager
await staking.setLockManager(owner, user2, { from: user1 })
await assertRevert(staking.canUnlock(user1, owner, user1, 0), STAKING_ERRORS.ERROR_LOCK_DOES_NOT_EXIST) // it doesnt exist
assert.equal(await staking.canUnlock(user1, owner, user2, 0), false, "User 1 can not unlock")
assert.equal(await staking.canUnlock(user2, owner, user2, 0), true, "User 2 can unlock")
})
it('fails to change lock manager if it doesnt exist', async () => {
await assertRevert(staking.setLockManager(owner, user2, { from: user1 }), STAKING_ERRORS.ERROR_LOCK_DOES_NOT_EXIST)
})
})

View File

@ -0,0 +1,115 @@
const { assertRevert } = require('@aragon/contract-helpers-test/assertThrow')
const { bn, assertBn } = require('@aragon/contract-helpers-test/numbers')
const { deploy } = require('../helpers/deploy')(artifacts)
const { approveAndStake } = require('../helpers/helpers')(artifacts)
const { DEFAULT_STAKE_AMOUNT, DEFAULT_LOCK_AMOUNT, EMPTY_DATA } = require('../helpers/constants')
const { STAKING_ERRORS, TIME_LOCK_MANAGER_ERRORS } = require('../helpers/errors')
const TimeLockManagerMock = artifacts.require('TimeLockManagerMock');
contract('Staking app, Time locking', ([owner]) => {
let token, staking, manager
const TIME_UNIT_BLOCKS = 0
const TIME_UNIT_SECONDS = 1
const DEFAULT_TIME = 1000
const DEFAULT_BLOCKS = 10
const approveStakeAndLock = async(unit, start, end, lockAmount = DEFAULT_LOCK_AMOUNT, stakeAmount = DEFAULT_STAKE_AMOUNT) => {
await approveAndStake({ staking, amount: stakeAmount, from: owner })
// allow manager
await staking.allowManager(manager.address, lockAmount, EMPTY_DATA)
// lock amount
await manager.lock(staking.address, owner, lockAmount, unit, start, end)
}
beforeEach(async () => {
const deployment = await deploy(owner)
token = deployment.token
staking = deployment.staking
manager = await TimeLockManagerMock.new()
})
it('locks using seconds', async () => {
const startTime = await manager.getTimestampExt()
const endTime = startTime.add(bn(DEFAULT_TIME))
await approveStakeAndLock(TIME_UNIT_SECONDS, startTime, endTime)
// check lock values
const { _amount, _allowance } = await staking.getLock(owner, manager.address)
assertBn(_amount, DEFAULT_LOCK_AMOUNT, "locked amount should match")
assertBn(_allowance, DEFAULT_LOCK_AMOUNT, "locked allowance should match")
// check time values
const { unit, start, end } = await manager.getTimeInterval(owner)
assert.equal(unit.toString(), TIME_UNIT_SECONDS.toString(), "interval unit should match")
assert.equal(start.toString(), startTime.toString(), "interval start should match")
assert.equal(end.toString(), endTime.toString(), "interval end should match")
// can not unlock
assert.equal(await staking.canUnlock(owner, owner, manager.address, 0), false, "Shouldn't be able to unlock")
assertBn(await staking.unlockedBalanceOf(owner), DEFAULT_STAKE_AMOUNT.sub(DEFAULT_LOCK_AMOUNT), "Unlocked balance should match")
await manager.setTimestamp(endTime.add(bn(1)))
// can unlock
assert.equal(await staking.canUnlock(owner, owner, manager.address, 0), true, "Should be able to unlock")
assertBn(await staking.unlockedBalanceOf(owner), DEFAULT_STAKE_AMOUNT.sub(DEFAULT_LOCK_AMOUNT), "Unlocked balance should match")
})
it('locks using blocks', async () => {
const startBlock = (await manager.getBlockNumberExt())
const endBlock = startBlock.add(bn(DEFAULT_BLOCKS))
await approveStakeAndLock(TIME_UNIT_BLOCKS, startBlock, endBlock)
// check lock values
const { _amount, _allowance } = await staking.getLock(owner, manager.address)
assertBn(_amount, DEFAULT_LOCK_AMOUNT, "locked amount should match")
assertBn(_allowance, DEFAULT_LOCK_AMOUNT, "locked allowance should match")
// check time values
const { unit, start, end } = await manager.getTimeInterval(owner)
assert.equal(unit.toString(), TIME_UNIT_BLOCKS.toString(), "interval unit should match")
assert.equal(start.toString(), startBlock.toString(), "interval start should match")
assert.equal(end.toString(), endBlock.toString(), "interval end should match")
// can not unlock
assert.equal(await staking.canUnlock(owner, owner, manager.address, 0), false, "Shouldn't be able to unlock")
assertBn(await staking.unlockedBalanceOf(owner), DEFAULT_STAKE_AMOUNT.sub(DEFAULT_LOCK_AMOUNT), "Unlocked balance should match")
await manager.setBlockNumber(endBlock.add(bn(1)))
// can unlock
assert.equal(await staking.canUnlock(owner, owner, manager.address, 0), true, "Should be able to unlock")
})
it('fails to unlock if can not unlock', async () => {
const startTime = await manager.getTimestampExt()
const endTime = startTime.add(bn(DEFAULT_TIME))
await approveStakeAndLock(TIME_UNIT_SECONDS, startTime, endTime)
// tries to unlock
await assertRevert(staking.unlockAndRemoveManager(owner, manager.address)/*, STAKING_ERRORS.ERROR_CANNOT_UNLOCK*/)
})
it('fails trying to lock twice', async () => {
const startTime = await manager.getTimestampExt()
const endTime = startTime.add(bn(DEFAULT_TIME))
await approveStakeAndLock(TIME_UNIT_SECONDS, startTime, endTime)
await assertRevert(manager.lock(staking.address, owner, DEFAULT_LOCK_AMOUNT, TIME_UNIT_SECONDS, startTime, endTime)/*, TIME_LOCK_MANAGER_ERRORS.ERROR_ALREADY_LOCKED*/)
})
it('fails trying to lock with wrong interval', async () => {
const startTime = await manager.getTimestampExt()
const endTime = startTime.add(bn(DEFAULT_TIME))
await ({ staking, amount: DEFAULT_STAKE_AMOUNT, from: owner })
// allow manager
await staking.allowManager(manager.address, DEFAULT_STAKE_AMOUNT, EMPTY_DATA)
// times are reverted!
await assertRevert(manager.lock(staking.address, owner, DEFAULT_LOCK_AMOUNT, TIME_UNIT_SECONDS, endTime, startTime)/*, TIME_LOCK_MANAGER_ERRORS.ERROR_WRONG_INTERVAL*/)
})
})

View File

@ -0,0 +1,176 @@
const { assertRevert } = require('@aragon/contract-helpers-test/assertThrow')
const { bn, assertBn, MAX_UINT64 } = require('@aragon/contract-helpers-test/numbers')
const { deploy } = require('./helpers/deploy')(artifacts)
const { approveAndStake } = require('./helpers/helpers')(artifacts)
const { DEFAULT_STAKE_AMOUNT, EMPTY_DATA } = require('./helpers/constants')
const { STAKING_ERRORS } = require('./helpers/errors')
const StakingMock = artifacts.require('StakingMock')
const StandardTokenMock = artifacts.require('StandardTokenMock');
const BadTokenMock = artifacts.require('BadTokenMock')
const getTokenBalance = async (token, account) => await token.balanceOf(account)
contract('Staking app', ([owner, other]) => {
let staking, token, stakingAddress, tokenAddress
beforeEach(async () => {
const initialAmount = DEFAULT_STAKE_AMOUNT.mul(bn(1000))
const tokenContract = await StandardTokenMock.new(owner, initialAmount)
token = tokenContract
tokenAddress = tokenContract.address
await token.mint(other, DEFAULT_STAKE_AMOUNT)
const stakingContract = await StakingMock.new(tokenAddress)
staking = stakingContract
stakingAddress = stakingContract.address
})
it('has correct initial state', async () => {
assert.equal(await staking.token(), tokenAddress, "Token is wrong")
assert.equal((await staking.totalStaked()).valueOf(), 0, "Initial total staked amount should be zero")
assert.equal(await staking.supportsHistory(), true, "history support should match")
})
it('fails deploying if token is not a contract', async() => {
await assertRevert(StakingMock.new(owner)/*, STAKING_ERRORS.ERROR_TOKEN_NOT_CONTRACT*/)
})
it('stakes', async () => {
const initialOwnerBalance = await getTokenBalance(token, owner)
const initialStakingBalance = await getTokenBalance(token, stakingAddress)
await approveAndStake({ staking, from: owner })
const finalOwnerBalance = await getTokenBalance(token, owner)
const finalStakingBalance = await getTokenBalance(token, stakingAddress)
assertBn(finalOwnerBalance, initialOwnerBalance.sub(bn(DEFAULT_STAKE_AMOUNT)), "owner balance should match")
assertBn(finalStakingBalance, initialStakingBalance.add(bn(DEFAULT_STAKE_AMOUNT)), "Staking app balance should match")
assertBn(await staking.totalStakedFor(owner), bn(DEFAULT_STAKE_AMOUNT), "staked value should match")
// total stake
assertBn(await staking.totalStaked(), bn(DEFAULT_STAKE_AMOUNT), "Total stake should match")
})
it('fails staking 0 amount', async () => {
await token.approve(stakingAddress, 1)
await assertRevert(staking.stake(0, EMPTY_DATA)/*, STAKING_ERRORS.ERROR_AMOUNT_ZERO*/)
})
it('fails staking more than balance', async () => {
const balance = await getTokenBalance(token, owner)
const amount = balance.add(bn(1))
await token.approve(stakingAddress, amount)
await assertRevert(staking.stake(amount, EMPTY_DATA)/*, STAKING_ERRORS.ERROR_TOKEN_DEPOSIT*/)
})
it('stakes for', async () => {
const initialOwnerBalance = await getTokenBalance(token, owner)
const initialOtherBalance = await getTokenBalance(token, other)
const initialStakingBalance = await getTokenBalance(token, stakingAddress)
// allow Staking app to move owner tokens
await token.approve(stakingAddress, DEFAULT_STAKE_AMOUNT)
// stake tokens
await staking.stakeFor(other, DEFAULT_STAKE_AMOUNT, EMPTY_DATA)
const finalOwnerBalance = await getTokenBalance(token, owner)
const finalOtherBalance = await getTokenBalance(token, other)
const finalStakingBalance = await getTokenBalance(token, stakingAddress)
assertBn(finalOwnerBalance, initialOwnerBalance.sub(bn(DEFAULT_STAKE_AMOUNT)), "owner balance should match")
assertBn(finalOtherBalance, initialOtherBalance, "other balance should match")
assertBn(finalStakingBalance, initialStakingBalance.add(bn(DEFAULT_STAKE_AMOUNT)), "Staking app balance should match")
assertBn(await staking.totalStakedFor(owner), bn(0), "staked value for owner should match")
assertBn(await staking.totalStakedFor(other), bn(DEFAULT_STAKE_AMOUNT), "staked value for other should match")
})
it('unstakes', async () => {
const initialOwnerBalance = await getTokenBalance(token, owner)
const initialStakingBalance = await getTokenBalance(token, stakingAddress)
await approveAndStake({ staking, from: owner })
// unstake half of them
await staking.unstake(DEFAULT_STAKE_AMOUNT.div(bn(2)), EMPTY_DATA)
const finalOwnerBalance = await getTokenBalance(token, owner)
const finalStakingBalance = await getTokenBalance(token, stakingAddress)
assertBn(finalOwnerBalance, initialOwnerBalance.sub(bn(DEFAULT_STAKE_AMOUNT.div(bn(2)))), "owner balance should match")
assertBn(finalStakingBalance, initialStakingBalance.add(bn(DEFAULT_STAKE_AMOUNT.div(bn(2)))), "Staking app balance should match")
assertBn(await staking.totalStakedFor(owner), bn(DEFAULT_STAKE_AMOUNT.div(bn(2))), "staked value should match")
})
it('fails unstaking 0 amount', async () => {
await approveAndStake({ staking, from: owner })
await assertRevert(staking.unstake(0, EMPTY_DATA)/*, STAKING_ERRORS.ERROR_AMOUNT_ZERO*/)
})
it('fails unstaking more than staked', async () => {
await approveAndStake({ staking, from: owner })
await assertRevert(staking.unstake(DEFAULT_STAKE_AMOUNT.add(bn(1)), EMPTY_DATA)/*, STAKING_ERRORS.ERROR_NOT_ENOUGH_BALANCE*/)
})
context('History', async () => {
it('supports history', async () => {
assert.equal(await staking.supportsHistory(), true, "It should support History")
})
it('has correct "last staked for"', async () => {
const blockNumber = await staking.getBlockNumberPublic()
const lastStaked = blockNumber.add(bn(5))
await staking.setBlockNumber(lastStaked)
await approveAndStake({ staking, from: owner })
assertBn(await staking.lastStakedFor(owner), lastStaked, "Last staked for should match")
})
it('has correct "total staked for at"', async () => {
const beforeBlockNumber = await staking.getBlockNumberPublic()
const lastStaked = beforeBlockNumber.add(bn(5))
await staking.setBlockNumber(lastStaked)
await approveAndStake({ staking, from: owner })
assertBn(await staking.totalStakedForAt(owner, beforeBlockNumber), bn(0), "Staked for at before staking should match")
assertBn(await staking.totalStakedForAt(owner, lastStaked), bn(DEFAULT_STAKE_AMOUNT), "Staked for after staking should match")
})
it('has correct "total staked at"', async () => {
const beforeBlockNumber = await staking.getBlockNumberPublic()
const lastStaked = beforeBlockNumber.add(bn(5))
await staking.setBlockNumber(lastStaked)
await approveAndStake({ staking, from: owner })
await approveAndStake({ staking, from: other })
assertBn(await staking.totalStakedAt(beforeBlockNumber), bn(0), "Staked for at before should match")
assertBn(await staking.totalStakedAt(lastStaked), bn(DEFAULT_STAKE_AMOUNT.mul(bn(2))), "Staked for at after staking should match")
})
it('fails to call totalStakedForAt with block number greater than max uint64', async () => {
await assertRevert(staking.totalStakedForAt(owner, MAX_UINT64.add(bn(1)))/*, STAKING_ERRORS.ERROR_BLOCKNUMBER_TOO_BIG*/)
})
it('fails to call totalStakedAt with block number greater than max uint64', async () => {
await assertRevert(staking.totalStakedAt(MAX_UINT64.add(bn(1)))/*, STAKING_ERRORS.ERROR_BLOCKNUMBER_TOO_BIG*/)
})
})
context('Bad Token', async () => {
let badStaking, badStakingAddress, badToken, badTokenAddress
beforeEach(async () => {
const initialAmount = DEFAULT_STAKE_AMOUNT.mul(bn(1000))
const tokenContract = await BadTokenMock.new(owner, initialAmount)
badToken = tokenContract
badTokenAddress = tokenContract.address
await badToken.mint(other, DEFAULT_STAKE_AMOUNT)
const stakingContract = await StakingMock.new(badTokenAddress)
badStaking = stakingContract
badStakingAddress = stakingContract.address
})
it('fails unstaking because of bad token', async () => {
// allow Staking app to move owner tokens
await badToken.approve(badStakingAddress, DEFAULT_STAKE_AMOUNT, { from: owner })
// stake tokens
await badStaking.stake(DEFAULT_STAKE_AMOUNT, EMPTY_DATA, { from: owner })
// unstake half of them, fails on token transfer
await assertRevert(badStaking.unstake(DEFAULT_STAKE_AMOUNT.div(bn(2)), EMPTY_DATA)/*, STAKING_ERRORS.ERROR_TOKEN_TRANSFER*/)
})
})
})

View File

@ -0,0 +1,116 @@
const { assertRevert } = require('@aragon/contract-helpers-test/assertThrow')
const { ZERO_ADDRESS } = require('./helpers/constants')
const { STAKING_ERRORS } = require('./helpers/errors')
const Staking = artifacts.require('Staking')
const StakingFactory = artifacts.require('StakingFactory')
const StandardTokenMock = artifacts.require('StandardTokenMock')
contract('StakingFactory', ([_, owner, someone]) => {
let token, factory, staking
const getInstance = receipt => receipt.logs.find(log => log.event === 'NewStaking').args.instance
beforeEach('deploy sample token and staking factory', async () => {
token = await StandardTokenMock.new(owner, 100000, { from: owner })
factory = await StakingFactory.new()
})
describe('getInstance', () => {
context('when the given token was not registered before', () => {
it('returns the zero address', async () => {
const instance = await factory.getInstance(token.address)
assert.equal(instance, ZERO_ADDRESS, 'instance address does not match')
})
})
context('when the given token was already registered', () => {
let instance
beforeEach('create staking instance', async () => {
instance = getInstance(await factory.getOrCreateInstance(token.address))
})
it('returns the corresponding staking instance address', async () => {
const foundInstance = await factory.getInstance(token.address)
assert.equal(instance, foundInstance, 'instance address does not match')
})
})
})
describe('existsInstance', () => {
context('when the given token was not registered before', () => {
it('returns false', async () => {
const exists = await factory.existsInstance(token.address)
assert(!exists, 'staking instance does exist')
})
})
context('when the given token was already registered', () => {
beforeEach('create staking instance', async () => {
await factory.getOrCreateInstance(token.address)
})
it('returns true', async () => {
const exists = await factory.existsInstance(token.address)
assert(exists, 'staking instance does not exist')
})
})
})
describe('getOrCreateInstance', () => {
context('when the given token was not registered before', () => {
context('when the given token is a contract', () => {
it('emits a NewStaking event', async () => {
const receipt = await factory.getOrCreateInstance(token.address)
const events = receipt.logs.filter(l => l.event === 'NewStaking')
assert.equal(events.length, 1, 'number of NewStaking events does not match')
assert.equal(events[0].args.token, token.address, 'token address does not match')
assert.notEqual(events[0].args.instance, ZERO_ADDRESS, 'instance address does not match')
})
it('creates a new staking instance', async () => {
const instance = getInstance(await factory.getOrCreateInstance(token.address))
staking = await Staking.at(instance)
assert.equal(await staking.token(), token.address, 'token address does not match')
})
})
context('when the given token is the zero address', () => {
const tokenAddress = ZERO_ADDRESS
it('reverts', async () => {
await assertRevert(factory.getOrCreateInstance(tokenAddress)/*, STAKING_ERRORS.ERROR_TOKEN_NOT_CONTRACT*/)
})
})
context('when the given token is not a contract', () => {
const tokenAddress = someone
it('reverts', async () => {
await assertRevert(factory.getOrCreateInstance(tokenAddress)/*, STAKING_ERRORS.ERROR_TOKEN_NOT_CONTRACT*/)
})
})
})
context('when the given token was already registered', () => {
let instance
beforeEach('create staking instance', async () => {
instance = getInstance(await factory.getOrCreateInstance(token.address))
})
it('does not create a new staking instance', async () => {
const receipt = await factory.getOrCreateInstance(token.address)
const events = receipt.logs.filter(l => l.event === 'NewStaking')
assert.equal(events.length, 0, 'number of NewStaking events does not match')
})
})
})
})

Some files were not shown because too many files have changed in this diff Show More