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:
parent
4344dc10c7
commit
c9639c3860
1
tests-solidity/.gitattributes
vendored
Normal file
1
tests-solidity/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sol linguist-language=Solidity
|
5
tests-solidity/.gitignore
vendored
Normal file
5
tests-solidity/.gitignore
vendored
Normal 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
102
tests-solidity/README.md
Normal 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"
|
||||
]
|
||||
}
|
||||
```
|
61
tests-solidity/init-test-node.sh
Executable file
61
tests-solidity/init-test-node.sh
Executable 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
|
15
tests-solidity/package.json
Normal file
15
tests-solidity/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
24
tests-solidity/suites/basic/contracts/Counter.sol
Normal file
24
tests-solidity/suites/basic/contracts/Counter.sol
Normal 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;
|
||||
}
|
||||
}
|
19
tests-solidity/suites/basic/contracts/test/Migrations.sol
Normal file
19
tests-solidity/suites/basic/contracts/test/Migrations.sol
Normal 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
const Migrations = artifacts.require("Migrations");
|
||||
|
||||
module.exports = function (deployer) {
|
||||
deployer.deploy(Migrations);
|
||||
};
|
14
tests-solidity/suites/basic/package.json
Normal file
14
tests-solidity/suites/basic/package.json
Normal 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"
|
||||
}
|
||||
}
|
0
tests-solidity/suites/basic/test/.gitkeep
Normal file
0
tests-solidity/suites/basic/test/.gitkeep
Normal file
80
tests-solidity/suites/basic/test/counter.js
Normal file
80
tests-solidity/suites/basic/test/counter.js
Normal 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)
|
||||
}
|
||||
})
|
||||
})
|
17
tests-solidity/suites/basic/truffle-config.js
Normal file
17
tests-solidity/suites/basic/truffle-config.js
Normal 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",
|
||||
},
|
||||
},
|
||||
}
|
3
tests-solidity/suites/initializable-buidler/.gitignore
vendored
Normal file
3
tests-solidity/suites/initializable-buidler/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Buidler
|
||||
artifacts
|
||||
cache
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
19
tests-solidity/suites/initializable-buidler/package.json
Normal file
19
tests-solidity/suites/initializable-buidler/package.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
const Migrations = artifacts.require("Migrations");
|
||||
|
||||
module.exports = function (deployer) {
|
||||
deployer.deploy(Migrations);
|
||||
};
|
16
tests-solidity/suites/initializable/package.json
Normal file
16
tests-solidity/suites/initializable/package.json
Normal 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"
|
||||
}
|
||||
}
|
0
tests-solidity/suites/initializable/test/.gitkeep
Normal file
0
tests-solidity/suites/initializable/test/.gitkeep
Normal file
74
tests-solidity/suites/initializable/test/lifecycle.js
Normal file
74
tests-solidity/suites/initializable/test/lifecycle.js
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
23
tests-solidity/suites/initializable/truffle-config.js
Normal file
23
tests-solidity/suites/initializable/truffle-config.js
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
31
tests-solidity/suites/proxy/contracts/DelegateProxy.sol
Normal file
31
tests-solidity/suites/proxy/contracts/DelegateProxy.sol
Normal 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) }
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
19
tests-solidity/suites/proxy/contracts/DepositableStorage.sol
Normal file
19
tests-solidity/suites/proxy/contracts/DepositableStorage.sol
Normal 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);
|
||||
}
|
||||
}
|
10
tests-solidity/suites/proxy/contracts/ERCProxy.sol
Normal file
10
tests-solidity/suites/proxy/contracts/ERCProxy.sol
Normal 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);
|
||||
}
|
21
tests-solidity/suites/proxy/contracts/IsContract.sol
Normal file
21
tests-solidity/suites/proxy/contracts/IsContract.sol
Normal 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;
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
8
tests-solidity/suites/proxy/contracts/test/EthSender.sol
Normal file
8
tests-solidity/suites/proxy/contracts/test/EthSender.sol
Normal file
@ -0,0 +1,8 @@
|
||||
pragma solidity 0.4.24;
|
||||
|
||||
|
||||
contract EthSender {
|
||||
function sendEth(address to) external payable {
|
||||
to.transfer(msg.value);
|
||||
}
|
||||
}
|
19
tests-solidity/suites/proxy/contracts/test/Migrations.sol
Normal file
19
tests-solidity/suites/proxy/contracts/test/Migrations.sol
Normal 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;
|
||||
}
|
||||
}
|
17
tests-solidity/suites/proxy/contracts/test/ProxyTarget.sol
Normal file
17
tests-solidity/suites/proxy/contracts/test/ProxyTarget.sol
Normal 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();
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
const Migrations = artifacts.require("Migrations");
|
||||
|
||||
module.exports = function (deployer) {
|
||||
deployer.deploy(Migrations);
|
||||
};
|
16
tests-solidity/suites/proxy/package.json
Normal file
16
tests-solidity/suites/proxy/package.json
Normal 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"
|
||||
}
|
||||
}
|
0
tests-solidity/suites/proxy/test/.gitkeep
Normal file
0
tests-solidity/suites/proxy/test/.gitkeep
Normal file
155
tests-solidity/suites/proxy/test/depositable_delegate_proxy.js
Normal file
155
tests-solidity/suites/proxy/test/depositable_delegate_proxy.js
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
23
tests-solidity/suites/proxy/truffle-config.js
Normal file
23
tests-solidity/suites/proxy/truffle-config.js
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
28
tests-solidity/suites/staking/.github/workflows/ci_contracts.yml
vendored
Normal file
28
tests-solidity/suites/staking/.github/workflows/ci_contracts.yml
vendored
Normal 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
|
648
tests-solidity/suites/staking/contracts/Staking.sol
Normal file
648
tests-solidity/suites/staking/contracts/Staking.sol
Normal 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)) }
|
||||
}
|
||||
}
|
44
tests-solidity/suites/staking/contracts/StakingFactory.sol
Normal file
44
tests-solidity/suites/staking/contracts/StakingFactory.sol
Normal 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);
|
||||
}
|
||||
}
|
155
tests-solidity/suites/staking/contracts/lib/Checkpointing.sol
Normal file
155
tests-solidity/suites/staking/contracts/lib/Checkpointing.sol
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
35
tests-solidity/suites/staking/contracts/lib/os/ERC20.sol
Normal file
35
tests-solidity/suites/staking/contracts/lib/os/ERC20.sol
Normal 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
|
||||
);
|
||||
}
|
13
tests-solidity/suites/staking/contracts/lib/os/ERCProxy.sol
Normal file
13
tests-solidity/suites/staking/contracts/lib/os/ERCProxy.sol
Normal 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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
91
tests-solidity/suites/staking/contracts/lib/os/SafeERC20.sol
Normal file
91
tests-solidity/suites/staking/contracts/lib/os/SafeERC20.sol
Normal 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;
|
||||
}
|
||||
}
|
73
tests-solidity/suites/staking/contracts/lib/os/SafeMath.sol
Normal file
73
tests-solidity/suites/staking/contracts/lib/os/SafeMath.sol
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
55
tests-solidity/suites/staking/contracts/standards/ERC900.sol
Normal file
55
tests-solidity/suites/staking/contracts/standards/ERC900.sol
Normal 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);
|
||||
}
|
23
tests-solidity/suites/staking/contracts/test/TestImports.sol
Normal file
23
tests-solidity/suites/staking/contracts/test/TestImports.sol
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
576
tests-solidity/suites/staking/contracts/test/lib/MiniMeToken.sol
Normal file
576
tests-solidity/suites/staking/contracts/test/lib/MiniMeToken.sol
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
26
tests-solidity/suites/staking/contracts/test/mocks/ERC20.sol
Normal file
26
tests-solidity/suites/staking/contracts/test/mocks/ERC20.sol
Normal 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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
18
tests-solidity/suites/staking/package.json
Normal file
18
tests-solidity/suites/staking/package.json
Normal 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"
|
||||
}
|
||||
}
|
52
tests-solidity/suites/staking/test/approve_and_call.js
Normal file
52
tests-solidity/suites/staking/test/approve_and_call.js
Normal 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*/)
|
||||
})
|
||||
})
|
83
tests-solidity/suites/staking/test/gas.js
Normal file
83
tests-solidity/suites/staking/test/gas.js
Normal 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)
|
||||
})
|
||||
*/
|
||||
})
|
10
tests-solidity/suites/staking/test/helpers/constants.js
Normal file
10
tests-solidity/suites/staking/test/helpers/constants.js
Normal 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'
|
||||
}
|
35
tests-solidity/suites/staking/test/helpers/deploy.js
Normal file
35
tests-solidity/suites/staking/test/helpers/deploy.js
Normal 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
|
||||
}
|
||||
}
|
35
tests-solidity/suites/staking/test/helpers/errors.js
Normal file
35
tests-solidity/suites/staking/test/helpers/errors.js
Normal 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,
|
||||
}
|
283
tests-solidity/suites/staking/test/helpers/helpers.js
Normal file
283
tests-solidity/suites/staking/test/helpers/helpers.js
Normal 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 doesn’t match')
|
||||
const balances = await staking.getBalancesOf(user.address)
|
||||
assertBn(user.stakedBalance, balances.staked, 'staked balance doesn’t match')
|
||||
assertBn(user.lockedBalance, balances.locked, 'locked balance doesn’t 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 doesn’t match')
|
||||
const token = await StandardTokenMock.at(await staking.token())
|
||||
const stakingTokenBalance = await token.balanceOf(staking.address)
|
||||
assertBn(totalStaked, stakingTokenBalance, 'Staking token balance doesn’t 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 can’t 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 can’t 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 can’t 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 can’t 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,
|
||||
}
|
||||
}
|
226
tests-solidity/suites/staking/test/lib/checkpointing.js
Normal file
226
tests-solidity/suites/staking/test/lib/checkpointing.js
Normal 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))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
308
tests-solidity/suites/staking/test/locking/funds_flows.js
Normal file
308
tests-solidity/suites/staking/test/locking/funds_flows.js
Normal 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 hasn’t 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 hasn’t 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 doesn’t allow to unlock', () => {
|
||||
moveFunds({ isContract: true, canUnlock: false })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('different origin and destiny', () => {
|
||||
context('when user hasn’t 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 hasn’t 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 doesn’t 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 })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
395
tests-solidity/suites/staking/test/locking/locking.js
Normal file
395
tests-solidity/suites/staking/test/locking/locking.js
Normal 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 doesn’t 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 doesn’t 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 doesn’t 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 doesn’t 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 doesn’t 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: it’s 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: it’s 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 doesn’t match")
|
||||
// lock manager
|
||||
assertBn(await staking.unlockedBalanceOf(user1), bn(0), "Unlocked balance should match")
|
||||
assertBn(await staking.lockedBalanceOf(user1), bn(0), "total locked doesn’t match")
|
||||
// recipient
|
||||
assertBn(await staking.unlockedBalanceOf(user2), transferAmount, "Unlocked balance should match")
|
||||
assertBn(await staking.lockedBalanceOf(user2), bn(0), "total locked doesn’t 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 doesn’t match")
|
||||
// lock manager
|
||||
assertBn(await staking.unlockedBalanceOf(user1), bn(0), "Unlocked balance should match")
|
||||
assertBn(await staking.lockedBalanceOf(user1), bn(0), "total locked doesn’t match")
|
||||
// recipient
|
||||
assertBn(await staking.unlockedBalanceOf(user2), transferAmount, "Unlocked balance should match")
|
||||
assertBn(await staking.lockedBalanceOf(user2), bn(0), "total locked doesn’t 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 doesn’t 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 doesn’t 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 doesn’t exist', async () => {
|
||||
await assertRevert(staking.setLockManager(owner, user2, { from: user1 }), STAKING_ERRORS.ERROR_LOCK_DOES_NOT_EXIST)
|
||||
})
|
||||
})
|
115
tests-solidity/suites/staking/test/locking/locking_time.js
Normal file
115
tests-solidity/suites/staking/test/locking/locking_time.js
Normal 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*/)
|
||||
})
|
||||
})
|
176
tests-solidity/suites/staking/test/staking.js
Normal file
176
tests-solidity/suites/staking/test/staking.js
Normal 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*/)
|
||||
})
|
||||
})
|
||||
})
|
116
tests-solidity/suites/staking/test/staking_factory.js
Normal file
116
tests-solidity/suites/staking/test/staking_factory.js
Normal 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
Loading…
Reference in New Issue
Block a user