diff --git a/README.md b/README.md index 3d42011..b6bfcfe 100644 --- a/README.md +++ b/README.md @@ -4,34 +4,17 @@ A Staking app with checkpointing (implementing ERC900 interface with history) and locking. +#### 📓 [Read the full documentation](/docs) + ## Testing ### Truffle Currently this app is using Truffle. You can run tests with `npm test`. -### Slither -[Install slither](https://github.com/trailofbits/slither#how-to-install) and then: -``` -slither --solc /usr/local/bin/solc . -``` - -Some noise can be filtered with: -``` -slither --solc /usr/local/bin/solc . 2>/tmp/a.txt ; grep -v "is not in mixedCase" /tmp/a.txt | grep "Contract: Staking" -``` - -### Echidna -Run `./scripts/flatten_echidna.sh` and then: -``` -docker run -v `pwd`:/src trailofbits/echidna echidna-test /src/flattened_contracts/EchidnaStaking.sol EchidnaStaking --config="/src/echidna/config.yaml" -``` - -### Manticore -``` -docker run --rm -ti -v `pwd`:/src trailofbits/manticore bash -ulimit -s unlimited -manticore --detect-all --contract Staking /src/flattened_contracts/Staking.sol -``` - -## Coverage +### Coverage You can measure coverage using Truffle by running `npm run coverage`. + +## Get involved +- Discuss in [Aragon Forum](https://forum.aragon.org/) +- Join the [Specturm chat](https://spectrum.chat/aragon?tab=posts) +- Join the [Discord chat](https://discord.gg/CMuvVY) diff --git a/contracts/Staking.sol b/contracts/Staking.sol index 75ecd90..eae6f10 100644 --- a/contracts/Staking.sol +++ b/contracts/Staking.sol @@ -302,10 +302,13 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IStakingLockin /** * @notice Change the manager of `_accountAddress`'s lock from `msg.sender` to `_newLockManager` * @param _accountAddress Owner of lock - * @param _newLockManager New lock's manager + * @param _newLockManager New lock manager */ function setLockManager(address _accountAddress, address _newLockManager) external isInitialized { - accounts[_accountAddress].locks[_newLockManager] = accounts[_accountAddress].locks[msg.sender]; + Lock storage lock = accounts[_accountAddress].locks[msg.sender]; + require(lock.allowance > 0, ERROR_LOCK_DOES_NOT_EXIST); + + accounts[_accountAddress].locks[_newLockManager] = lock; delete accounts[_accountAddress].locks[msg.sender]; @@ -340,7 +343,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IStakingLockin /** * @notice Get total amount of locked tokens for `_accountAddress` * @param _accountAddress Owner of locks - * @return Total amount of locked tokens + * @return Total amount of locked tokens for the requested account */ function getTotalLockedOf(address _accountAddress) external view isInitialized returns (uint256) { return _getTotalLockedOf(_accountAddress); @@ -351,7 +354,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IStakingLockin * @param _accountAddress Owner of lock * @param _lockManager Manager of the lock for the given account * @return Amount of locked tokens - * @return Lock's data + * @return Amount of tokens that lock manager is allowed to lock */ function getLock(address _accountAddress, address _lockManager) external @@ -367,11 +370,34 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IStakingLockin _allowance = lock.allowance; } + /** + * @notice Get staked and locked balances of `_accountAddress` + * @param _accountAddress Account being requested + * @return Amount of staked tokens + * @return Amount of total locked tokens + */ function getBalancesOf(address _accountAddress) external view returns (uint256 staked, uint256 locked) { - staked = totalStakedFor(_accountAddress); + staked = _totalStakedFor(_accountAddress); locked = _getTotalLockedOf(_accountAddress); } + /** + * @notice Get the amount of tokens staked by `_accountAddress` + * @param _accountAddress The owner of the tokens + * @return The amount of tokens staked by the given account + */ + function totalStakedFor(address _accountAddress) external view isInitialized returns (uint256) { + return _totalStakedFor(_accountAddress); + } + + /** + * @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 `_accountAddress` at block number `_blockNumber` * @param _accountAddress Account requesting for @@ -404,6 +430,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IStakingLockin * @notice Check if `_accountAddress`'s by `_lockManager` can be unlocked * @param _accountAddress Owner of lock * @param _lockManager Manager of the lock for the given account + * @param _amount Amount of tokens to be potentially unlocked. If zero, it means the whole locked amount * @return Whether given lock of given account can be unlocked */ function canUnlock(address _accountAddress, address _lockManager, uint256 _amount) external view isInitialized returns (bool) { @@ -426,25 +453,6 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IStakingLockin _stakeFor(_from, _from, _amount, _data); } - /** - * @notice Get the amount of tokens staked by `_accountAddress` - * @param _accountAddress The owner of the tokens - * @return The amount of tokens staked by the given account - */ - function totalStakedFor(address _accountAddress) public view returns (uint256) { - // we assume it's not possible to stake in the future - return accounts[_accountAddress].stakedHistory.getLatestValue(); - } - - /** - * @notice Get the total amount of tokens staked by all users - * @return The total amount of tokens staked by all users - */ - function totalStaked() public view returns (uint256) { - // we assume it's not possible to stake in the future - return totalStakedHistory.getLatestValue(); - } - /* function multicall(bytes[] _calls) public { for(uint i = 0; i < _calls.length; i++) { @@ -485,7 +493,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IStakingLockin } function _modifyStakeBalance(address _accountAddress, uint256 _by, bool _increase) internal returns (uint256) { - uint256 currentStake = totalStakedFor(_accountAddress); + uint256 currentStake = _totalStakedFor(_accountAddress); uint256 newStake; if (_increase) { @@ -502,7 +510,7 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IStakingLockin } function _modifyTotalStaked(uint256 _by, bool _increase) internal { - uint256 currentStake = totalStaked(); + uint256 currentStake = _totalStaked(); uint256 newStake; if (_increase) { @@ -585,13 +593,32 @@ contract Staking is Autopetrified, ERCStaking, ERCStakingHistory, IStakingLockin emit StakeTransferred(_from, _to, _amount); } + /** + * @notice Get the amount of tokens staked by `_accountAddress` + * @param _accountAddress The owner of the tokens + * @return The amount of tokens staked by the given account + */ + function _totalStakedFor(address _accountAddress) internal view returns (uint256) { + // we assume it's not possible to stake in the future + return accounts[_accountAddress].stakedHistory.getLatestValue(); + } + + /** + * @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.getLatestValue(); + } + /** * @notice Get the staked but unlocked amount of tokens by `_accountAddress` * @param _accountAddress Owner of the staked but unlocked balance * @return Amount of tokens staked but not locked by given account */ function _unlockedBalanceOf(address _accountAddress) internal view returns (uint256) { - uint256 unlockedTokens = totalStakedFor(_accountAddress).sub(accounts[_accountAddress].totalLocked); + uint256 unlockedTokens = _totalStakedFor(_accountAddress).sub(accounts[_accountAddress].totalLocked); return unlockedTokens; } diff --git a/contracts/test/EchidnaStaking.sol b/contracts/test/EchidnaStaking.sol index b498112..534659c 100644 --- a/contracts/test/EchidnaStaking.sol +++ b/contracts/test/EchidnaStaking.sol @@ -19,7 +19,7 @@ contract EchidnaStaking is Staking { address _account = msg.sender; Account storage account = accounts[_account]; - if (totalStakedFor(_account) < account.totalLocked) { + if (_totalStakedFor(_account) < account.totalLocked) { return false; } @@ -31,7 +31,7 @@ contract EchidnaStaking is Staking { address _account = msg.sender; Account storage account = accounts[_account]; - if (totalStakedFor(_account) > account.totalLocked) { + if (_totalStakedFor(_account) > account.totalLocked) { return false; } @@ -63,7 +63,7 @@ contract EchidnaStaking is Staking { // total staked matches less or equal than token balance function echidna_total_staked_is_balance() external view returns (bool) { - if (totalStaked() <= stakingToken.balanceOf(this)) { + if (_totalStaked() <= stakingToken.balanceOf(this)) { return true; } diff --git a/docs/1-anti-sybil/README.md b/docs/1-anti-sybil/README.md new file mode 100644 index 0000000..d1500a8 --- /dev/null +++ b/docs/1-anti-sybil/README.md @@ -0,0 +1,10 @@ +# Anti-sybil + +Staking app uses the [Checkpointing library](https://github.com/aragon/aragon-apps/pull/415) to provide a history of balances within the app. This is important for applications such as token-weighted voting, as one token could potentially be used to vote more than once if it’s transferred to another account after being used to cast a vote. Mimicing the [MiniMe token](https://github.com/Giveth/minime), checkpointing allows to have a snapshot of balances at any given used time, that can be used to tally votes. + +Any time that there is a balance change in the Staking app, the Checkpointing library stores the timestamp and value in an array for the balance owner. Balance changes are stored in natural time order, meaning that it’s not possible to modify a balance nor add a checkpoint in the past. + +One important technical detail to note is that in order to save gas (Checkpointing is an expensive operation), timestamp and value are stored together in one slot, so the maximum amount that can be stored is `2^192 - 1`, which may break compatibility with common ERC-20 tokens and be problematic in some edge-cases. + +There is also a history array for the total staked in the app. + diff --git a/docs/2-slashing/README.md b/docs/2-slashing/README.md new file mode 100644 index 0000000..69ee572 --- /dev/null +++ b/docs/2-slashing/README.md @@ -0,0 +1,8 @@ +# Slashing + +Many use cases need to be able to slash users’ tokens depending on the outcome of certain actions. For instance, when proposing an action in the Agreements app, if it’s challenged and the [Aragon Court](https://court.aragon.org/dashboard) resolves to accept the challenge, user would lose the staked collateral. + +Staking app achieves this by adding locks on top of staked balances. A user can designate a lock manager, which can be either a contract or an `EOA`, and a maximum allowance for that manager. The manager then will be able to lock up to that allowed amount of tokens, and to unlock them too. While the tokens are locked, the original owner cannot unstake them, while the manager can transfer to wherever is needed: another user’s lock, the staked balance of another user, or even the external token balance of another account. + +If the manager is a contract, it must implement the method `canUnlock`, where certain conditions can be specified to allow the owner to re-gain control of the tokens. A common use case would be a time based lock manager, that would lock tokens only for some period, and once the period is over, the user would be able to unlock and unstake the tokens. + diff --git a/docs/3-entry-points/README.md b/docs/3-entry-points/README.md new file mode 100644 index 0000000..1762f2a --- /dev/null +++ b/docs/3-entry-points/README.md @@ -0,0 +1,503 @@ +# Entry points + +### initialize + +This is used by the Staking Factory when creating a new proxy. See deployment section for more details. + +- **Actor:** Deployer account +- **Inputs:** + - **_stakingToken: ** ERC20 token to be used for staking +- **Authentication:** Open +- **Pre-flight checks:** + - Checks that input token is a contract +- **State transitions:** + - Sets the staking token + - Marks the contract as initialized + + +## Staking ERC900 interface + +### stake + +Stakes `_amount` tokens, transferring them from `msg.sender` + +- **Actor:** Staking user +- **Inputs:** + - **_amount:** Number of tokens staked + - **_data:** Used in Staked event, to add signalling information in more complex staking applications +- **Authentication:** Open +- **Pre-flight checks:** + - Checks that contract has been initialized + - Checks that amount is not zero +- **State transitions:** + - Transfers tokens from sender to contract + - Increments sender’s balance + - Increments total balance + +### stakeFor + +Stakes `_amount` tokens, transferring them from caller, and assigns them to `_accountAddress` + +- **Actor:** Staking user +- **Inputs:** + - **_accountAddress:** The final staker of the tokens + - **_amount:** Number of tokens staked + - **_data:** Used in Staked event, to add signalling information in more complex staking applications +- **Authentication:** Open +- **Pre-flight checks:** + - Checks that contract has been initialized + - Checks that amount is not zero +- **State transitions:** + - Transfers tokens from sender to contract + - Increments final recipient’s balance + - Increments total balance + + +### unstake + +Unstakes `_amount` tokens, returning them to the user + +- **Actor:** Staking user +- **Inputs:** + - **_amount:** Number of tokens to unstake + - **_data:** Used in Unstaked event, to add signalling information in more complex staking applications +- **Authentication:** Open +- **Pre-flight checks:** + - Checks that contract has been initialized + - Checks that amount is not zero +- **State transitions:** + - Transfers tokens from contract to sender + - Decrements user’s balance + - Decrements total balance + + +### token + +Get the token used by the contract for staking and locking + +- **Actor:** Any +- **Outputs:** + - Address of the staking token +- **Authentication:** Open +- **Pre-flight checks:** + - Checks that contract has been initialized + + +### supportsHistory + +It returns true, as it supports history of stakes + +- **Actor:** Any +- **Outputs:** + - true +- **Authentication:** Open + + +### lastStakedFor + +Get last time `_accountAddress` modified its staked balance + +- **Actor:** Any +- **Inputs:** + - **_accountAddress:** Account requesting for +- **Outputs:** + - Last block number when account’s balance was modified +- **Authentication:** Open +- **Pre-flight checks:** + - Checks that contract has been initialized + + +### totalStakedForAt + +Get the total amount of tokens staked by `_accountAddress` at block number `_blockNumber` + +- **Actor:** Any +- **Inputs:** + - **_accountAddress:** Account requesting for + - **_blockNumber:** Block number at which we are requesting +- **Outputs:** + - The amount of tokens staked by the account at the given block number +- **Authentication:** Open +- **Pre-flight checks:** + - Checks that contract has been initialized + + +### totalStakedAt + +Get the total amount of tokens staked by all users at block number `_blockNumber` + +- **Actor:** Any +- **Inputs:** + - **_blockNumber:** Block number at which we are requesting +- **Outputs:** + - The amount of tokens staked at the given block number +- **Authentication:** Open +- **Pre-flight checks:** + - Checks that contract has been initialized + + +### totalStakedFor + +Get the amount of tokens staked by `_accountAddress` + +- **Actor:** Any +- **Inputs:** + - **_accountAddress:** Account requesting for +- **Outputs:** + - The amount of tokens staked by the given account +- **Authentication:** Open +- **Pre-flight checks:** + - Checks that contract has been initialized + +### totalStaked + +Get the total amount of tokens staked by all users + +- **Actor:** Any +- **Outputs:** + - The total amount of tokens staked by all users +- **Authentication:** Open +- **Pre-flight checks:** + - Checks that contract has been initialized + + +## Locking interface + +### allowManager + +Allow `_lockManager` to lock up to `_allowance` tokens of `msg.sender` +It creates a new lock, so the lock for this manager cannot exist before. + +- **Actor:** Staking user +- **Inputs:** + - **_lockManager:** The manager entity for this particular lock + - **_allowance:** Amount of tokens that the manager can lock + - **_data:** Used in `NewLockManager` event and to parametrize logic for the lock to be enforced by the manager +- **Authentication:** Open. Implicitly, sender must be staking owner. +- **Pre-flight checks:** + - Checks that contract has been initialized + - Checks that allowance input is not zero + - Checks that lock didn’t exist before +- **State transitions:** + - Sets allowance for the pair owner-manager to the given amount + - Calls lock manager callback + +### allowManagerAndLock + +Lock `_amount` staked tokens and assign `_lockManager` as manager with `_allowance` allowed tokens and `_data` as data, so they can not be unstaked + +- **Actor:** Staking user (owner) +- **Inputs:** + - **_amount:** The amount of tokens to be locked + - **_lockManager:** The manager entity for this particular lock + - **_allowance:** Amount of tokens that the manager can lock + - **_data:** Used in `NewLockManager` event and to parametrize logic for the lock to be enforced by the manager +- **Authentication:** Open. Implicitly, sender must be staking owner. +- **Pre-flight checks:** + - Checks that contract has been initialized + - Checks that allowance input is not zero + - Checks that lock didn’t exist before + - Checks that amount input is not zero + - Checks that user has enough unlocked tokens available + - Checks that amount is not greater than allowance +- **State transitions:** + - Sets allowance for the pair owner-manager to the given amount + - Sets the amount of tokens as locked by the manager + - Increases the total amount of locked tokens balance for the user + - Calls lock manager callback + + +### transfer + +Transfer `_amount` tokens to `_to`’s staked balance + +- **Actor:** Staking user (owner) +- **Inputs:** + - **_to:** Recipient of the tokens + - **_amount:** Number of tokens to be transferred +- **Authentication:** Open. Implicitly, sender must be staking owner. +- **Pre-flight checks:** + - Checks that contract has been initialized + - Checks that amount input is not zero + - Checks that user has enough unlocked tokens available +- **State transitions:** + - Decreases sender balance + - Increases recipient balance + +### transferAndUnstake + +Transfer `_amount` tokens to `_to`’s external balance (i.e. unstaked) + +- **Actor:** Staking user (owner) +- **Inputs:** + - **_to:** Recipient of the tokens + - **_amount:** Number of tokens to be transferred +- **Authentication:** Open. Implicitly, sender must be staking owner. +- **Pre-flight checks:** + - Checks that contract has been initialized + - Checks that amount input is not zero + - Checks that user has enough unlocked tokens available +- **State transitions:** + - Decreases sender balance + - Makes a token transfer from Stakig to recipient’s account + - Decreases Staking total balance + +### slash + +Transfer `_amount` tokens from `_from`'s lock by `msg.sender` to `_to` + +- **Actor:** Lock manager +- **Inputs:** + - **_from:** Owner of locked tokens + - **_to:** Recipient of the tokens + - **_amount:** Number of tokens to be transferred +- **Authentication:** Open. Implicitly, sender must be lock manager. +- **Pre-flight checks:** + - Checks that contract has been initialized + - Check that owner’s lock is enough + - Checks that amount input is not zero +- **State transitions:** + - Decreases owner’s locked amount for the calle lock manager + - Decreases owner’s total locked amount + - Decreases owner balance + - Increases recipient balance + +### slashAndUnstake + +Transfer `_amount` tokens from `_from`'s lock by `msg.sender` to `_to`’s external wallet (unstaked) + +- **Actor:** Lock manager +- **Inputs:** + - **_from:** Owner of locked tokens + - **_to:** Recipient of the tokens + - **_amount:** Number of tokens to be transferred +- **Authentication:** Open. Implicitly, sender must be lock manager +- **Pre-flight checks:** + - Checks that contract has been initialized + - Check that owner’s lock is enough + - Checks that amount input is not zero +- **State transitions:** + - Decreases owner’s locked amount for the caller lock manager + - Decreases owner’s total locked amount + - Decreases owner balance + - Makes a token transfer from Stakig to recipient’s account + - Decreases Staking total balance + +### slashAndUnlock + +Transfer `_transferAmount` tokens from `_from`'s lock by `msg.sender` to `_to`, and decrease `_decreaseAmount` tokens from that lock + +- **Actor:** Lock manager +- **Inputs:** + - **_from:** Owner of locked tokens + - **_to:** Recipient of the tokens + - **_unlockAmount:** Number of tokens to be unlocked + - **_slashAmount:** Number of tokens to be transferred +- **Authentication:** Open. Implicitly, sender must be lock manager +- **Pre-flight checks:** + - Checks that contract has been initialized + - Check that owner’s lock is enough + - Checks that unlock amount input is not zero + - Checks that slash amount input is not zero +- **State transitions:** + - Decreases owner’s locked amount for the caller lock manager by both input amounts + - Decreases owner’s total locked amount by both input amounts + - Decreases owner balance by slash amount + - Increases recipient balance by slash amount + + +### increaseLockAllowance + +Increase allowance in `_allowance` tokens of lock manager `_lockManager` for user `msg.sender` + +- **Actor:** Staking user (owner) +- **Inputs:** + - **_lockManager:** The manager entity for this particular lock + - **_allowance:** Amount of allowed tokens increase +- **Authentication:** Open. Implicitly, sender must be staking owner. +- **Pre-flight checks:** + - Checks that contract has been initialized + - Checks that lock exists (i.e., it has a previous allowance) + - Checks that amount input is not zero +- **State transitions:** + - Increases lock allowance for this pair of owner and lock manager + +### decreaseLockAllowance + +Decrease allowance in `_allowance` tokens of lock manager `_lockManager` for user `_accountAddress` + +- **Actor:** Staking user (owner) or lock manager +- **Inputs:** + - **_accountAddress:** Owner of locked tokens + - **_lockManager:** The manager entity for this particular lock + - **_allowance:** Amount of allowed tokens decrease +- **Authentication:** Only owner or lock manager +- **Pre-flight checks:** + - Checks that contract has been initialized + - Checks that amount input is not zero + - Checks that lock exists (i.e., it has a previous allowance) + - Checks that final allowed amount is not less than currently locked tokens + - Checks that final allowed result is not zero (unlockAndRemoveManager must be used for this) +- **State transitions:** + - Decreases lock allowance for this pair of owner and lock manager + + +### lock + +Increase locked amount by `_amount` tokens for user `_accountAddress` by lock manager `_lockManager` + +- **Actor:** Staking user (owner) or lock manager +- **Inputs:** + - **_accountAddress:** Owner of locked tokens + - **_lockManager:** The manager entity for this particular lock + - **_amount:** Amount of locked tokens increase +- **Authentication:** Only owner or lock manager +- **Pre-flight checks:** + - Checks that contract has been initialized + - Checks that amount input is not zero + - Checks that user has enough unlocked tokens available + - Checks that lock has enough allowance +- **State transitions:** + - Increases locked tokens for this pair of owner and lock manager + - Increases owner’s total locked tokens + +### unlock + +Decrease locked amount by `_amount` tokens for user `_accountAddress` by lock manager `_lockManager` + +- **Actor:** Staking user (owner) or lock manager +- **Inputs:** + - **_accountAddress:** Owner of locked tokens + - **_lockManager:** The manager entity for this particular lock + - **_amount:** Amount of locked tokens decrease +- **Authentication:** Only owner or lock manager +- **Pre-flight checks:** + - Checks that contract has been initialized + - Checks that amount input is not zero + - Checks that lock exists (i.e., it has a previous allowance) + - Checks that user has enough unlocked tokens available + - If sender is owner, checks that manager allows to unlock +- **State transitions:** + - Decreases locked tokens for this pair of owner and lock manager + - Decreases owner’s total locked tokens + + +### unlockAndRemoveManager + +Unlock `_accountAddress`'s lock by `_lockManager` so locked tokens can be unstaked again + +- **Actor:** Staking user (owner) or lock manager +- **Inputs:** + - **_accountAddress:** Owner of locked tokens + - **_lockManager:** The manager entity for this particular lock +- **Authentication:** Only owner or lock manager +- **Pre-flight checks:** + - Checks that contract has been initialized + - Checks that lock exists (i.e., it has a previous allowance) + - If sender is owner and there were locked tokens, checks that manager allows to unlock +- **State transitions:** + - Decreases owner’s total locked amount by currently locked tokens for this lock manager + - Deletes lock for this pair of owner and lock manager + +### setLockManager + +Change the manager of `_accountAddress`'s lock from `msg.sender` to `_newLockManager` + +- **Actor:** Lock manager +- **Inputs:** + - **_accountAddress:** Owner of lock + - **_newLockManager:** New lock manager +- **Authentication:** Open. Implicitly, sender must be lock manager +- **Pre-flight checks:** + - Checks that contract has been initialized + - Checks that lock exists +- **State transitions:** + - Assigns lock to new manager + +### getTotalLockedOf + +Get total amount of locked tokens for `_accountAddress` + +- **Actor:** Any +- **Inputs:** + - **_accountAddress:** Owner of locks +- **Outputs:** + - Total amount of locked tokens for the requested account +- **Authentication:** Open + + +### getLock + +Get details of `_accountAddress`'s lock by `_lockManager` + +- **Actor:** Any +- **Inputs:** + - **_accountAddress:** Owner of lock + - **_lockManager:** Manager of the lock for the given account +- **Outputs:** + - **_amount:** Amount of locked tokens + - **_allowance:** Amount of tokens that lock manager is allowed to lock +- **Authentication:** Open + + +### getBalancesOf + +Get staked and locked balances of `_accountAddress` + +- **Actor:** Any +- **Inputs:** + - **_accountAddress:** Account being requested +- **Outputs:** + - **staked:** Amount of staked tokens + - **locked:** Amount of total locked tokens +- **Authentication:** Open + + +### unlockedBalanceOf + +Get the staked but unlocked amount of tokens by `_accountAddress` + +- **Actor:** Any +- **Inputs:** + - **_accountAddress:** Owner of the staked but unlocked balance +- **Outputs:** + - Amount of tokens staked but not locked by given account +- **Authentication:** Open + + +### canUnlock + +Check if `_accountAddress`'s by `_lockManager` can be unlocked + +- **Actor:** Any +- **Inputs:** + - **_accountAddress:** Owner of lock + - **_lockManager:** Manager of the lock for the given account + - **_amount:** Amount of tokens to be potentially unlocked. If zero, it means the whole locked amount +- **Outputs:** + - True if caller is allowed to unlock the requested amount (all the lock if amount requested is zero) +- **Authentication:** Open +- **Pre-flight checks:** + - It will revert if the lock doesn’t exist or if the requested amount is greater than the lock. + +## MiniMe callback + +### receiveApproval + +MiniMeToken ApproveAndCallFallBack compliance + +- **Actor:** Staking token +- **Inputs:** + - **_from:** Account approving tokens + - **_amount:** Amount of `_token` tokens being approved + - **_token:** MiniMeToken that is being approved and that the call comes from + - **_data:** Used in Staked event, to add signalling information in more complex staking applications +- **Authentication:** It must be called by the staking token +- **Pre-flight checks:** + - Check that `_token` parameter, Staking token and caller are all the same +- **State transitions:** + - Transfers tokens from `_from` account to contract + - Increments `_from` account’s balance + - Increments total balance + + diff --git a/docs/4-data-structures/README.md b/docs/4-data-structures/README.md new file mode 100644 index 0000000..5007b30 --- /dev/null +++ b/docs/4-data-structures/README.md @@ -0,0 +1,17 @@ +# Data structures + +## Account + +It stores all the information related to users: + +- `locks`: A mapping from manager to Lock (see below). So there will be an entry for each manager locking tokens of this account owner +- `totalLocked`: the current total amount of tokens locked by all the managers +- `stakedHistory`: link to the Checkpointing library to keep staking history + +## Lock + +Stores the information for a particular pair of owner and manager: + +- `amount`: The currently locked amount +- `allowance`: The maximum amount that lock manager can lock for the owner of the lock. Must be greater than zero to consider the lock active, and always greater than or equal to amount + diff --git a/docs/5-external-interface/README.md b/docs/5-external-interface/README.md new file mode 100644 index 0000000..8f96371 --- /dev/null +++ b/docs/5-external-interface/README.md @@ -0,0 +1,248 @@ +# External interface + +### Initialize + +- **Name:** initialize +- **Inputs:** + - **_stakingToken: ** ERC20 token to be used for staking + + +## Staking ERC900 interface + +### Stake + +- **Name:** stake +- **Inputs:** + - **_amount:** Number of tokens staked + - **_data:** Used in Staked event, to add signalling information in more complex staking applications + +### Stake for + +- **Name:** stakeFor +- **Inputs:** + - **_accountAddress:** The final staker of the tokens + - **_amount:** Number of tokens staked + - **_data:** Used in Staked event, to add signalling information in more complex staking applications + +### Unstake + +- **Name:** unstake +- **Inputs:** + - **_amount:** Number of tokens to unstake + - **_data:** Used in Unstaked event, to add signalling information in more complex staking applications + +### Token + +- **Name:** token +- **Outputs:** + - Address of the staking token + +### Supports history + +- **Name:** supportsHistory +- **Outputs:** + - true + +### Last staked for + +- **Name:** lastStakedFor +- **Inputs:** + - **_accountAddress:** Account requesting for +- **Outputs:** + - Last block number when account’s balance was modified + +### Total staked for at + +- **Name:** totalStakedForAt +- **Inputs:** + - **_accountAddress:** Account requesting for + - **_blockNumber:** Block number at which we are requesting +- **Outputs:** + - The amount of tokens staked by the account at the given block number + +### Total staked at + +- **Name:** totalStakedAt +- **Inputs:** + - **_blockNumber:** Block number at which we are requesting +- **Outputs:** + - The amount of tokens staked at the given block number + +### Total staked for + +- **Name:** totalStakedFor +- **Inputs:** + - **_accountAddress:** Account requesting for +- **Outputs:** + - The amount of tokens staked by the given account + +### Total staked + +- **Name:** totalStaked +- **Outputs:** + - The total amount of tokens staked by all users + +## Locking interface + +### Allow new manager + +- **Name:** allowManager +- **Inputs:** + - **_lockManager:** The manager entity for this particular lock + - **_allowance:** Amount of tokens that the manager can lock + - **_data:** Used in `NewLockManager` event and to parametrize logic for the lock to be enforced by the manager + +### Allow new manager and lock + +- **Name:** allowManagerAndLock +- **Inputs:** + - **_amount:** The amount of tokens to be locked + - **_lockManager:** The manager entity for this particular lock + - **_allowance:** Amount of tokens that the manager can lock + - **_data:** Used in `NewLockManager` event and to parametrize logic for the lock to be enforced by the manager + +### Transfer + +- **Name:** transfer +- **Inputs:** + - **_to:** Recipient of the tokens + - **_amount:** Number of tokens to be transferred + +### Transfer and unstake + +- **Name:** transferAndUnstake +- **Inputs:** + - **_to:** Recipient of the tokens + - **_amount:** Number of tokens to be transferred + +### Slash + +- **Name:** slash +- **Inputs:** + - **_from:** Owner of locked tokens + - **_to:** Recipient of the tokens + - **_amount:** Number of tokens to be transferred + +### Slash and unstake + +- **Name:** slashAndUnstake +- **Inputs:** + - **_from:** Owner of locked tokens + - **_to:** Recipient of the tokens + - **_amount:** Number of tokens to be transferred + +### Slash and unlock + +- **Name:** slashAndUnlock +- **Inputs:** + - **_from:** Owner of locked tokens + - **_to:** Recipient of the tokens + - **_unlockAmount:** Number of tokens to be unlocked + - **_slashAmount:** Number of tokens to be transferred + +### Increase lock allowance + +- **Name:** increaseLockAllowance +- **Inputs:** + - **_lockManager:** The manager entity for this particular lock + - **_allowance:** Amount of allowed tokens increase + +### Decrease lock allowance + +- **Name:** decreaseLockAllowance +- **Inputs:** + - **_accountAddress:** Owner of locked tokens + - **_lockManager:** The manager entity for this particular lock + - **_allowance:** Amount of allowed tokens decrease + +### Lock + +- **Name:** lock +- **Inputs:** + - **_accountAddress:** Owner of locked tokens + - **_lockManager:** The manager entity for this particular lock + - **_amount:** Amount of locked tokens increase + +### Unlock + +- **Name:** unlock +- **Inputs:** + - **_accountAddress:** Owner of locked tokens + - **_lockManager:** The manager entity for this particular lock + - **_amount:** Amount of locked tokens decrease + +### Unlock and remove manager + +- **Name:** unlockAndRemoveManager +- **Inputs:** + - **_accountAddress:** Owner of locked tokens + - **_lockManager:** The manager entity for this particular lock + +### Set lock manager + +- **Name:** setLockManager +- **Inputs:** + - **_accountAddress:** Owner of lock + - **_newLockManager:** New lock manager + +### Get total locked of + +- **Name:** getTotalLockedOf +- **Inputs:** + - **_accountAddress:** Owner of locks +- **Outputs:** + - Total amount of locked tokens for the requested account + + +### Get lock + +- **Name:** getLock +- **Inputs:** + - **_accountAddress:** Owner of lock + - **_lockManager:** Manager of the lock for the given account +- **Outputs:** + - **_amount:** Amount of locked tokens + - **_allowance:** Amount of tokens that lock manager is allowed to lock + + +### Get balances of + +- **Name:** getBalancesOf +- **Inputs:** + - **_accountAddress:** Account being requested +- **Outputs:** + - **staked:** Amount of staked tokens + - **locked:** Amount of total locked tokens + + +### Unlocked balance of + +- **Name:** unlockedBalanceOf +- **Inputs:** + - **_accountAddress:** Owner of the staked but unlocked balance +- **Outputs:** + - Amount of tokens staked but not locked by given account + + +### Can unlock + +- **Name:** canUnlock +- **Inputs:** + - **_accountAddress:** Owner of lock + - **_lockManager:** Manager of the lock for the given account + - **_amount:** Amount of tokens to be potentially unlocked. If zero, it means the whole locked amount +- **Outputs:** + - True if caller is allowed to unlock the requested amount (all the lock if amount requested is zero) + +## MiniMe callback + +### Receive approval + +- **Name:** receiveApproval +- **Inputs:** + - **_from:** Account approving tokens + - **_amount:** Amount of `_token` tokens being approved + - **_token:** MiniMeToken that is being approved and that the call comes from + - **_data:** Used in Staked event, to add signalling information in more complex staking applications + + diff --git a/docs/6-deployment/README.md b/docs/6-deployment/README.md new file mode 100644 index 0000000..e0b064d --- /dev/null +++ b/docs/6-deployment/README.md @@ -0,0 +1,3 @@ +# Deployment + +TODO diff --git a/docs/7-testing-guide/README.md b/docs/7-testing-guide/README.md new file mode 100644 index 0000000..ad51985 --- /dev/null +++ b/docs/7-testing-guide/README.md @@ -0,0 +1,3 @@ +# Testing guide + +TODO diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..cf77380 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,19 @@ +The Staking app complies with [interface ERC900](https://eips.ethereum.org/EIPS/eip-900) with the following added features: + +- Anti-sybil protection +- Slashing mechanism + +The main motivation is to be used in conjunction with [Agreements](https://github.com/aragon/aragon-apps/tree/master/apps/agreement) in the context of Aragon Network, but it has been designed to be as generic as possible, in order to allow for other use cases too. + +You can read the initial motivation and spec for this [here](https://forum.aragon.org/t/staking-locks-spec-v2/217). + +## Table of Contents + +1. [Anti-sybil protection](./1-anti-sybil) +2. [Slashing mechanism](./2-slashing) +3. [Entry points](./3-entry-points) +4. [Data structures](./4-data-structures) +5. [External interface](./5-external-interface) +6. [Deployment](./6-deployment) +7. [Testing guide](./7-testing-guide) + diff --git a/test/locking.js b/test/locking.js index a66df74..89b0f10 100644 --- a/test/locking.js +++ b/test/locking.js @@ -381,4 +381,8 @@ contract('Staking app, Locking', ([owner, user1, user2]) => { assert.equal(await staking.canUnlock(owner, user2, 0, { from: user1 }), false, "User 1 can not unlock") assert.equal(await staking.canUnlock(owner, user2, 0, { from: user2 }), 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) + }) })