laconicd/tests-solidity/suites/staking/contracts/Staking.sol
Brett Sun c9639c3860
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>
2020-09-01 17:16:28 -04:00

649 lines
26 KiB
Solidity
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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