New modification of WorkLockPoolingContract - without worklock part

pull/2544/head
vzotova 2021-01-29 16:30:49 +03:00
parent cf4d0db4ce
commit 436ae0f134
3 changed files with 824 additions and 0 deletions

View File

@ -0,0 +1 @@
New preferable base pooling contract

View File

@ -0,0 +1,297 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.7.0;
import "zeppelin/ownership/Ownable.sol";
import "zeppelin/math/SafeMath.sol";
import "contracts/staking_contracts/AbstractStakingContract.sol";
/**
* @notice Contract acts as delegate for sub-stakers
**/
contract PoolingStakingContractV2 is InitializableStakingContract, Ownable {
using SafeMath for uint256;
using Address for address payable;
using SafeERC20 for NuCypherToken;
event TokensDeposited(
address indexed sender,
uint256 value,
uint256 depositedTokens
);
event TokensWithdrawn(
address indexed sender,
uint256 value,
uint256 depositedTokens
);
event ETHWithdrawn(address indexed sender, uint256 value);
event DepositSet(address indexed sender, bool value);
struct Delegator {
uint256 depositedTokens;
uint256 withdrawnReward;
uint256 withdrawnETH;
}
/// Defines base fraction and precision of worker fraction. Value 100 defines 1 worker fraction is 1% of reward
uint256 public constant BASIS_FRACTION = 100;
StakingEscrow public escrow;
address public workerOwner;
uint256 public totalDepositedTokens;
uint256 public totalWithdrawnReward;
uint256 public totalWithdrawnETH;
uint256 workerFraction;
uint256 public workerWithdrawnReward;
mapping(address => Delegator) public delegators;
bool public depositIsEnabled = true;
/**
* @notice Initialize function for using with OpenZeppelin proxy
* @param _workerFraction Share of token reward that worker node owner will get.
* Use value up to BASIS_FRACTION, if _workerFraction = BASIS_FRACTION -> means 100% reward as commission
* @param _router StakingInterfaceRouter address
* @param _workerOwner Owner of worker node, only this address can withdraw worker commission
*/
function initialize(
uint256 _workerFraction,
StakingInterfaceRouter _router,
address _workerOwner
) public initializer {
require(_workerOwner != address(0) && _workerFraction <= BASIS_FRACTION);
InitializableStakingContract.initialize(_router);
_transferOwnership(msg.sender);
escrow = _router.target().escrow();
workerFraction = _workerFraction;
workerOwner = _workerOwner;
}
/**
* @notice Enabled deposit
*/
function enableDeposit() external onlyOwner {
depositIsEnabled = true;
emit DepositSet(msg.sender, depositIsEnabled);
}
/**
* @notice Disable deposit
*/
function disableDeposit() external onlyOwner {
depositIsEnabled = false;
emit DepositSet(msg.sender, depositIsEnabled);
}
/**
* @notice Calculate worker's fraction depending on deposited tokens
*/
function getWorkerFraction() public view returns (uint256) {
return workerFraction;
}
/**
* @notice Transfer tokens as delegator
* @param _value Amount of tokens to transfer
*/
function depositTokens(uint256 _value) external {
require(depositIsEnabled, "Deposit must be enabled");
require(_value > 0, "Value must be not empty");
totalDepositedTokens = totalDepositedTokens.add(_value);
Delegator storage delegator = delegators[msg.sender];
delegator.depositedTokens = delegator.depositedTokens.add(_value);
token.safeTransferFrom(msg.sender, address(this), _value);
emit TokensDeposited(msg.sender, _value, delegator.depositedTokens);
}
/**
* @notice Get available reward for all delegators and owner
*/
function getAvailableReward() public view returns (uint256) {
uint256 stakedTokens = escrow.getAllTokens(address(this));
uint256 freeTokens = token.balanceOf(address(this));
uint256 reward = stakedTokens.add(freeTokens).sub(totalDepositedTokens);
if (reward > freeTokens) {
return freeTokens;
}
return reward;
}
/**
* @notice Get cumulative reward
*/
function getCumulativeReward() public view returns (uint256) {
return getAvailableReward().add(totalWithdrawnReward);
}
/**
* @notice Get available reward in tokens for worker node owner
*/
function getAvailableWorkerReward() public view returns (uint256) {
uint256 reward = getCumulativeReward();
uint256 maxAllowableReward;
if (totalDepositedTokens != 0) {
uint256 fraction = getWorkerFraction();
maxAllowableReward = reward.mul(fraction).div(BASIS_FRACTION);
} else {
maxAllowableReward = reward;
}
if (maxAllowableReward > workerWithdrawnReward) {
return maxAllowableReward - workerWithdrawnReward;
}
return 0;
}
/**
* @notice Get available reward in tokens for delegator
*/
function getAvailableReward(address _delegator)
public
view
returns (uint256)
{
if (totalDepositedTokens == 0) {
return 0;
}
uint256 reward = getCumulativeReward();
Delegator storage delegator = delegators[_delegator];
uint256 fraction = getWorkerFraction();
uint256 maxAllowableReward = reward.mul(delegator.depositedTokens).mul(BASIS_FRACTION - fraction).div(
totalDepositedTokens.mul(BASIS_FRACTION)
);
return
maxAllowableReward > delegator.withdrawnReward
? maxAllowableReward - delegator.withdrawnReward
: 0;
}
/**
* @notice Withdraw reward in tokens to worker node owner
*/
function withdrawWorkerReward() external {
require(msg.sender == workerOwner);
uint256 balance = token.balanceOf(address(this));
uint256 availableReward = getAvailableWorkerReward();
if (availableReward > balance) {
availableReward = balance;
}
require(
availableReward > 0,
"There is no available reward to withdraw"
);
workerWithdrawnReward = workerWithdrawnReward.add(availableReward);
totalWithdrawnReward = totalWithdrawnReward.add(availableReward);
token.safeTransfer(msg.sender, availableReward);
emit TokensWithdrawn(msg.sender, availableReward, 0);
}
/**
* @notice Withdraw reward to delegator
* @param _value Amount of tokens to withdraw
*/
function withdrawTokens(uint256 _value) public override {
uint256 balance = token.balanceOf(address(this));
require(_value <= balance, "Not enough tokens in the contract");
Delegator storage delegator = delegators[msg.sender];
uint256 availableReward = getAvailableReward(msg.sender);
require( _value <= availableReward, "Requested amount of tokens exceeded allowed portion");
delegator.withdrawnReward = delegator.withdrawnReward.add(_value);
totalWithdrawnReward = totalWithdrawnReward.add(_value);
token.safeTransfer(msg.sender, _value);
emit TokensWithdrawn(msg.sender, _value, delegator.depositedTokens);
}
/**
* @notice Withdraw reward, deposit and fee to delegator
*/
function withdrawAll() public {
uint256 balance = token.balanceOf(address(this));
Delegator storage delegator = delegators[msg.sender];
uint256 availableReward = getAvailableReward(msg.sender);
uint256 value = availableReward.add(delegator.depositedTokens);
require(value <= balance, "Not enough tokens in the contract");
// TODO remove double reading
uint256 availableWorkerReward = getAvailableWorkerReward();
// potentially could be less then due reward
uint256 availableETH = getAvailableETH(msg.sender);
// prevent losing reward for worker after calculations
uint256 workerReward = availableWorkerReward.mul(delegator.depositedTokens).div(totalDepositedTokens);
if (workerReward > 0) {
require(value.add(workerReward) <= balance, "Not enough tokens in the contract");
token.safeTransfer(workerOwner, workerReward);
emit TokensWithdrawn(workerOwner, workerReward, 0);
}
uint256 withdrawnToDecrease = workerWithdrawnReward.mul(delegator.depositedTokens).div(totalDepositedTokens);
workerWithdrawnReward = workerWithdrawnReward.sub(withdrawnToDecrease);
totalWithdrawnReward = totalWithdrawnReward.sub(withdrawnToDecrease).sub(delegator.withdrawnReward);
totalDepositedTokens = totalDepositedTokens.sub(delegator.depositedTokens);
delegator.withdrawnReward = 0;
delegator.depositedTokens = 0;
token.safeTransfer(msg.sender, value);
emit TokensWithdrawn(msg.sender, value, 0);
totalWithdrawnETH = totalWithdrawnETH.sub(delegator.withdrawnETH);
delegator.withdrawnETH = 0;
if (availableETH > 0) {
msg.sender.sendValue(availableETH);
emit ETHWithdrawn(msg.sender, availableETH);
}
}
/**
* @notice Get available ether for delegator
*/
function getAvailableETH(address _delegator) public view returns (uint256) {
Delegator storage delegator = delegators[_delegator];
uint256 balance = address(this).balance;
// ETH balance + already withdrawn
balance = balance.add(totalWithdrawnETH);
uint256 maxAllowableETH = balance.mul(delegator.depositedTokens).div(totalDepositedTokens);
uint256 availableETH = maxAllowableETH.sub(delegator.withdrawnETH);
if (availableETH > balance) {
availableETH = balance;
}
return availableETH;
}
/**
* @notice Withdraw available amount of ETH to delegator
*/
function withdrawETH() public override {
Delegator storage delegator = delegators[msg.sender];
uint256 availableETH = getAvailableETH(msg.sender);
require(availableETH > 0, "There is no available ETH to withdraw");
delegator.withdrawnETH = delegator.withdrawnETH.add(availableETH);
totalWithdrawnETH = totalWithdrawnETH.add(availableETH);
msg.sender.sendValue(availableETH);
emit ETHWithdrawn(msg.sender, availableETH);
}
/**
* @notice Calling fallback function is allowed only for the owner
*/
function isFallbackAllowed() public override view returns (bool) {
return msg.sender == owner();
}
}

View File

@ -0,0 +1,526 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from eth_tester.exceptions import TransactionFailed
from web3 import Web3
from web3.contract import Contract
from nucypher.blockchain.eth.token import NU
WORKER_FRACTION = 10
BASIS_FRACTION = 100
@pytest.fixture()
def pooling_contract(testerchain, router, deploy_contract):
owner = testerchain.client.accounts[1]
worker_owner = testerchain.client.accounts[2]
contract, _ = deploy_contract('PoolingStakingContractV2')
# Initialize
tx = contract.functions.initialize(WORKER_FRACTION, router.address, worker_owner).transact({'from': owner})
testerchain.wait_for_receipt(tx)
return contract
@pytest.fixture()
def pooling_contract_interface(testerchain, staking_interface, pooling_contract):
return testerchain.client.get_contract(
abi=staking_interface.abi,
address=pooling_contract.address,
ContractFactoryClass=Contract)
def test_staking(testerchain, token_economics, token, escrow, pooling_contract, pooling_contract_interface):
creator = testerchain.client.accounts[0]
owner = testerchain.client.accounts[1]
worker_owner = testerchain.client.accounts[2]
delegators = testerchain.client.accounts[3:6]
deposit_log = pooling_contract.events.TokensDeposited.createFilter(fromBlock='latest')
withdraw_log = pooling_contract.events.TokensWithdrawn.createFilter(fromBlock='latest')
assert pooling_contract.functions.owner().call() == owner
assert pooling_contract.functions.workerOwner().call() == worker_owner
assert pooling_contract.functions.getWorkerFraction().call() == WORKER_FRACTION
assert pooling_contract.functions.workerWithdrawnReward().call() == 0
assert pooling_contract.functions.totalDepositedTokens().call() == 0
assert pooling_contract.functions.totalWithdrawnReward().call() == 0
assert token.functions.balanceOf(pooling_contract.address).call() == 0
assert pooling_contract.functions.getAvailableWorkerReward().call() == 0
assert pooling_contract.functions.getAvailableReward().call() == 0
# Give some tokens to delegators
for index, delegator in enumerate(delegators):
tokens = token_economics.minimum_allowed_locked // (index + 1) * 2
tx = token.functions.transfer(delegator, tokens).transact({'from': creator})
testerchain.wait_for_receipt(tx)
# Delegators deposit tokens to the pooling contract
total_deposited_tokens = 0
tokens_supply = 0
assert pooling_contract.functions.getAvailableReward().call() == 0
for index, delegator in enumerate(delegators):
assert pooling_contract.functions.delegators(delegator).call() == [0, 0, 0]
assert pooling_contract.functions.getAvailableReward(delegator).call() == 0
tokens = token.functions.balanceOf(delegator).call() // 2
tx = token.functions.approve(pooling_contract.address, tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.depositTokens(tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.delegators(delegator).call() == [tokens, 0, 0]
assert pooling_contract.functions.getAvailableReward(delegator).call() == 0
total_deposited_tokens += tokens
tokens_supply += tokens
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert token.functions.balanceOf(pooling_contract.address).call() == tokens_supply
events = deposit_log.get_all_entries()
assert len(events) == index + 1
event_args = events[-1]['args']
assert event_args['sender'] == delegator
assert event_args['value'] == tokens
assert event_args['depositedTokens'] == tokens
assert pooling_contract.functions.getWorkerFraction().call() == WORKER_FRACTION
assert pooling_contract.functions.workerWithdrawnReward().call() == 0
assert pooling_contract.functions.totalWithdrawnReward().call() == 0
assert pooling_contract.functions.getAvailableWorkerReward().call() == 0
assert pooling_contract.functions.getAvailableReward().call() == 0
# Disable deposit
log = pooling_contract.events.DepositSet.createFilter(fromBlock='latest')
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.disableDeposit().transact({'from': delegators[0]})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.disableDeposit().transact({'from': owner})
testerchain.wait_for_receipt(tx)
events = log.get_all_entries()
assert len(events) == 1
event_args = events[-1]['args']
assert event_args['sender'] == owner
assert not event_args['value']
delegator = delegators[0]
tokens = token.functions.balanceOf(delegator).call()
tx = token.functions.approve(pooling_contract.address, tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.depositTokens(tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
tx = token.functions.approve(pooling_contract.address, 0).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
# Enable deposit
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.enableDeposit().transact({'from': delegators[0]})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.enableDeposit().transact({'from': owner})
testerchain.wait_for_receipt(tx)
events = log.get_all_entries()
assert len(events) == 2
event_args = events[-1]['args']
assert event_args['sender'] == owner
assert event_args['value']
# Delegators deposit tokens to the pooling contract again
for index, delegator in enumerate(delegators):
tokens = token.functions.balanceOf(delegator).call()
tx = token.functions.approve(pooling_contract.address, tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.depositTokens(tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.delegators(delegator).call() == [2 * tokens, 0, 0]
total_deposited_tokens += tokens
tokens_supply += tokens
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert token.functions.balanceOf(pooling_contract.address).call() == tokens_supply
events = deposit_log.get_all_entries()
assert len(events) == len(delegators) + index + 1
event_args = events[-1]['args']
assert event_args['sender'] == delegator
assert event_args['value'] == tokens
assert event_args['depositedTokens'] == 2 * tokens
assert pooling_contract.functions.totalWithdrawnReward().call() == 0
# Only owner can deposit tokens to the staking escrow
stake = tokens_supply
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract_interface.functions.depositAsStaker(stake, 5).transact({'from': delegators[0]})
testerchain.wait_for_receipt(tx)
tx = pooling_contract_interface.functions.depositAsStaker(stake, 5).transact({'from': owner})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert token.functions.balanceOf(pooling_contract.address).call() == 0
# Give some tokens as a reward
assert pooling_contract.functions.getAvailableReward().call() == 0
reward = token_economics.minimum_allowed_locked
tx = token.functions.approve(escrow.address, reward).transact()
testerchain.wait_for_receipt(tx)
tx = escrow.functions.deposit(pooling_contract.address, reward, 0).transact()
testerchain.wait_for_receipt(tx)
# Only owner can withdraw tokens from the staking escrow
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract_interface.functions.withdrawAsStaker(reward).transact({'from': delegators[0]})
testerchain.wait_for_receipt(tx)
withdrawn_stake = reward + stake
assert pooling_contract.functions.getAvailableReward().call() == 0
tx = pooling_contract_interface.functions.withdrawAsStaker(withdrawn_stake).transact({'from': owner})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.getAvailableReward().call() == reward
worker_reward = reward * WORKER_FRACTION // BASIS_FRACTION
assert pooling_contract.functions.getAvailableWorkerReward().call() == worker_reward
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert token.functions.balanceOf(pooling_contract.address).call() == withdrawn_stake
tokens_supply = withdrawn_stake
total_withdrawn_tokens = 0
# Each delegator can withdraw some portion of tokens
available_reward = reward
for index, delegator in enumerate(delegators):
deposited_tokens = pooling_contract.functions.delegators(delegator).call()[0]
max_portion = reward * deposited_tokens * (BASIS_FRACTION - WORKER_FRACTION) // \
(total_deposited_tokens * BASIS_FRACTION)
# Can't withdraw more than max allowed
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.withdrawTokens(max_portion + 1).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
portion = max_portion // 2
assert pooling_contract.functions.getAvailableReward(delegator).call() == max_portion
tx = pooling_contract.functions.withdrawTokens(portion).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.delegators(delegator).call() == [deposited_tokens, portion, 0]
assert pooling_contract.functions.getAvailableReward(delegator).call() == max_portion - portion
tokens_supply -= portion
total_withdrawn_tokens += portion
available_reward -= portion
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert token.functions.balanceOf(pooling_contract.address).call() == tokens_supply
assert token.functions.balanceOf(delegator).call() == portion
assert pooling_contract.functions.totalWithdrawnReward().call() == total_withdrawn_tokens
events = withdraw_log.get_all_entries()
assert len(events) == index + 1
event_args = events[-1]['args']
assert event_args['sender'] == delegator
assert event_args['value'] == portion
assert event_args['depositedTokens'] == deposited_tokens
# Node owner withdraws tokens
assert pooling_contract.functions.getAvailableWorkerReward().call() == worker_reward
assert pooling_contract.functions.getAvailableReward().call() == available_reward
# Only node owner can call this method
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.withdrawWorkerReward().transact({'from': delegators[0]})
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.withdrawWorkerReward().transact({'from': owner})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.withdrawWorkerReward().transact({'from': worker_owner})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.getWorkerFraction().call() == WORKER_FRACTION
assert pooling_contract.functions.workerWithdrawnReward().call() == worker_reward
assert pooling_contract.functions.getAvailableWorkerReward().call() == 0
tokens_supply -= worker_reward
total_withdrawn_tokens += worker_reward
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert token.functions.balanceOf(pooling_contract.address).call() == tokens_supply
assert token.functions.balanceOf(owner).call() == 0
assert token.functions.balanceOf(worker_owner).call() == worker_reward
assert pooling_contract.functions.totalWithdrawnReward().call() == total_withdrawn_tokens
assert pooling_contract.functions.getAvailableReward().call() == available_reward - worker_reward
events = withdraw_log.get_all_entries()
assert len(events) == len(delegators) + 1
event_args = events[-1]['args']
assert event_args['sender'] == worker_owner
assert event_args['value'] == worker_reward
assert event_args['depositedTokens'] == 0
# Can't withdraw more than max allowed
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.withdrawWorkerReward().transact({'from': worker_owner})
testerchain.wait_for_receipt(tx)
# Each delegator can withdraw rest of reward and deposit
previous_total_deposited_tokens = total_deposited_tokens
withdrawn_worker_reward = worker_reward
# Withdraw everything from one delegator and check others rewards
delegator = delegators[0]
deposited_tokens = pooling_contract.functions.delegators(delegator).call()[0]
withdrawn_tokens = pooling_contract.functions.delegators(delegator).call()[1]
max_portion = reward * deposited_tokens * (BASIS_FRACTION - WORKER_FRACTION) // \
(previous_total_deposited_tokens * BASIS_FRACTION)
supposed_portion = max_portion // 2
reward_portion = pooling_contract.functions.getAvailableReward(delegator).call()
# could be some rounding errors
assert abs(supposed_portion - reward_portion) <= 10
new_portion = deposited_tokens + reward_portion
previous_portion = token.functions.balanceOf(delegator).call()
tx = pooling_contract.functions.withdrawAll().transact({'from': delegator})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.delegators(delegator).call() == [0, 0, 0]
tokens_supply -= new_portion
total_deposited_tokens -= deposited_tokens
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert token.functions.balanceOf(pooling_contract.address).call() == tokens_supply
assert token.functions.balanceOf(delegator).call() == previous_portion + new_portion
assert token.functions.balanceOf(worker_owner).call() == worker_reward
assert pooling_contract.functions.getAvailableReward(delegator).call() == 0
withdraw_to_decrease = withdrawn_worker_reward * deposited_tokens // previous_total_deposited_tokens
total_withdrawn_tokens -= withdraw_to_decrease
total_withdrawn_tokens -= withdrawn_tokens
withdrawn_worker_reward -= withdraw_to_decrease
assert pooling_contract.functions.totalWithdrawnReward().call() == total_withdrawn_tokens
assert abs(pooling_contract.functions.workerWithdrawnReward().call() - withdrawn_worker_reward) <= 1
events = withdraw_log.get_all_entries()
assert len(events) == len(delegators) + 2
event_args = events[-1]['args']
assert event_args['sender'] == delegator
assert event_args['value'] == new_portion
assert event_args['depositedTokens'] == 0
# Check worker's reward, still zero
assert pooling_contract.functions.getAvailableWorkerReward().call() == 0
# Check others rewards
for delegator in delegators[1:3]:
deposited_tokens = pooling_contract.functions.delegators(delegator).call()[0]
max_portion = reward * deposited_tokens * (BASIS_FRACTION - WORKER_FRACTION) // \
(previous_total_deposited_tokens * BASIS_FRACTION)
supposed_portion = max_portion // 2
reward_portion = pooling_contract.functions.getAvailableReward(delegator).call()
# could be some rounding errors
assert abs(supposed_portion - reward_portion) <= 10
# Increase reward for delegators and worker
new_reward = token_economics.minimum_allowed_locked // 2
tx = token.functions.transfer(pooling_contract.address, new_reward).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tokens_supply += new_reward
new_worker_reward = new_reward * WORKER_FRACTION // BASIS_FRACTION
assert new_worker_reward > 0
assert abs(pooling_contract.functions.getAvailableWorkerReward().call() - new_worker_reward) <= 1
new_worker_reward = pooling_contract.functions.getAvailableWorkerReward().call()
# Withdraw everything from one delegator and check others rewards
previous_total_deposited_tokens = total_deposited_tokens
delegator = delegators[1]
deposited_tokens = pooling_contract.functions.delegators(delegator).call()[0]
withdrawn_tokens = pooling_contract.functions.delegators(delegator).call()[1]
reward_portion = pooling_contract.functions.getAvailableReward(delegator).call()
other_reward_portion = pooling_contract.functions.getAvailableReward(delegators[2]).call()
new_portion = deposited_tokens + reward_portion
previous_portion = token.functions.balanceOf(delegator).call()
tx = pooling_contract.functions.withdrawAll().transact({'from': delegator})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.delegators(delegator).call() == [0, 0, 0]
tokens_supply -= new_portion
total_deposited_tokens -= deposited_tokens
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
new_worker_transfer = new_worker_reward * deposited_tokens // previous_total_deposited_tokens
tokens_supply -= new_worker_transfer
assert token.functions.balanceOf(pooling_contract.address).call() == tokens_supply
assert token.functions.balanceOf(delegator).call() == previous_portion + new_portion
assert token.functions.balanceOf(worker_owner).call() == worker_reward + new_worker_transfer
assert pooling_contract.functions.getAvailableReward(delegator).call() == 0
withdraw_to_decrease = withdrawn_worker_reward * deposited_tokens // previous_total_deposited_tokens
total_withdrawn_tokens -= withdraw_to_decrease
total_withdrawn_tokens -= withdrawn_tokens
withdrawn_worker_reward -= withdraw_to_decrease
assert pooling_contract.functions.totalWithdrawnReward().call() == total_withdrawn_tokens
assert abs(pooling_contract.functions.workerWithdrawnReward().call() - withdrawn_worker_reward) <= 1
events = withdraw_log.get_all_entries()
assert len(events) == len(delegators) + 4
event_args = events[-2]['args']
assert event_args['sender'] == worker_owner
assert event_args['value'] == new_worker_transfer
assert event_args['depositedTokens'] == 0
event_args = events[-1]['args']
assert event_args['sender'] == delegator
assert event_args['value'] == new_portion
assert event_args['depositedTokens'] == 0
# Check worker's reward
new_worker_reward = new_worker_reward * (previous_total_deposited_tokens - deposited_tokens) // previous_total_deposited_tokens
assert pooling_contract.functions.getAvailableWorkerReward().call() == new_worker_reward
# Check others rewards
assert abs(pooling_contract.functions.getAvailableReward(delegators[2]).call() - other_reward_portion) <= 10
# Withdraw last portion for last delegator
delegator = delegators[2]
deposited_tokens = pooling_contract.functions.delegators(delegator).call()[0]
reward_portion = pooling_contract.functions.getAvailableReward(delegator).call()
new_portion = deposited_tokens + reward_portion
previous_portion = token.functions.balanceOf(delegator).call()
tx = pooling_contract.functions.withdrawAll().transact({'from': delegator})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.delegators(delegator).call() == [0, 0, 0]
assert pooling_contract.functions.totalDepositedTokens().call() == 0
assert token.functions.balanceOf(pooling_contract.address).call() <= 1
assert token.functions.balanceOf(delegator).call() == previous_portion + new_portion
assert token.functions.balanceOf(worker_owner).call() == worker_reward + new_worker_transfer + new_worker_reward
assert pooling_contract.functions.getAvailableReward().call() <= 1
events = withdraw_log.get_all_entries()
assert len(events) == len(delegators) + 6
event_args = events[-2]['args']
assert event_args['sender'] == worker_owner
assert event_args['value'] == new_worker_reward
assert event_args['depositedTokens'] == 0
event_args = events[-1]['args']
assert event_args['sender'] == delegator
assert event_args['value'] == new_portion
assert event_args['depositedTokens'] == 0
def test_fee(testerchain, token_economics, token, policy_manager, pooling_contract, pooling_contract_interface):
creator = testerchain.client.accounts[0]
owner = testerchain.client.accounts[1]
delegators = testerchain.client.accounts[2:5]
withdraw_log = pooling_contract.events.ETHWithdrawn.createFilter(fromBlock='latest')
assert pooling_contract.functions.getWorkerFraction().call() == WORKER_FRACTION
assert pooling_contract.functions.totalDepositedTokens().call() == 0
assert pooling_contract.functions.totalWithdrawnETH().call() == 0
assert token.functions.balanceOf(pooling_contract.address).call() == 0
# Give some tokens to delegators and deposit them
for index, delegator in enumerate(delegators):
tokens = token_economics.minimum_allowed_locked // (index + 1)
tx = token.functions.transfer(delegator, tokens).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = token.functions.approve(pooling_contract.address, tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.depositTokens(tokens).transact({'from': delegator})
testerchain.wait_for_receipt(tx)
total_deposited_tokens = pooling_contract.functions.totalDepositedTokens().call()
assert pooling_contract.functions.totalWithdrawnETH().call() == 0
assert testerchain.client.get_balance(pooling_contract.address) == 0
# Give some fees
value = Web3.toWei(1, 'ether')
tx = testerchain.client.send_transaction(
{'from': testerchain.client.coinbase, 'to': policy_manager.address, 'value': value})
testerchain.wait_for_receipt(tx)
# Only owner can withdraw fees from the policy manager
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract_interface.functions.withdrawPolicyFee().transact({'from': delegators[0]})
testerchain.wait_for_receipt(tx)
balance = testerchain.client.get_balance(owner)
tx = pooling_contract_interface.functions.withdrawPolicyFee().transact({'from': owner})
testerchain.wait_for_receipt(tx)
assert testerchain.client.get_balance(pooling_contract.address) == value
assert testerchain.client.get_balance(owner) == balance
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
withdrawn_eth = 0
eth_supply = value
# Each delegator can withdraw portion of eth
for index, delegator in enumerate(delegators):
deposited_tokens = pooling_contract.functions.delegators(delegator).call()[0]
max_portion = value * deposited_tokens // total_deposited_tokens
balance = testerchain.client.get_balance(delegator)
assert pooling_contract.functions.getAvailableETH(delegator).call() == max_portion
tx = pooling_contract.functions.withdrawETH().transact({'from': delegator, 'gas_price': 0})
testerchain.wait_for_receipt(tx)
assert pooling_contract.functions.delegators(delegator).call() == [deposited_tokens, 0, max_portion]
eth_supply -= max_portion
withdrawn_eth += max_portion
assert pooling_contract.functions.totalDepositedTokens().call() == total_deposited_tokens
assert testerchain.client.get_balance(pooling_contract.address) == eth_supply
assert testerchain.client.get_balance(delegator) == balance + max_portion
assert pooling_contract.functions.totalWithdrawnETH().call() == withdrawn_eth
# Can't withdraw more than max allowed
with pytest.raises((TransactionFailed, ValueError)):
tx = pooling_contract.functions.withdrawETH().transact({'from': delegator})
testerchain.wait_for_receipt(tx)
events = withdraw_log.get_all_entries()
assert len(events) == index + 1
event_args = events[-1]['args']
assert event_args['sender'] == delegator
assert event_args['value'] == max_portion
def test_reentrancy(testerchain, pooling_contract, token, deploy_contract):
creator = testerchain.client.accounts[0]
owner = testerchain.client.accounts[1]
# Prepare contracts
reentrancy_contract, _ = deploy_contract('ReentrancyTest')
contract_address = reentrancy_contract.address
tx = pooling_contract.functions.transferOwnership(contract_address).transact({'from': owner})
testerchain.wait_for_receipt(tx)
# Transfer ETH to the contract
value = Web3.toWei(1, 'ether')
tx = testerchain.client.send_transaction(
{'from': testerchain.client.coinbase, 'to': pooling_contract.address, 'value': value})
testerchain.wait_for_receipt(tx)
assert testerchain.client.get_balance(pooling_contract.address) == value
# Change eth distribution, owner will be able to withdraw only half
tokens = WORKER_FRACTION
tx = token.functions.transfer(owner, tokens).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = token.functions.approve(pooling_contract.address, tokens).transact({'from': owner})
testerchain.wait_for_receipt(tx)
tx = pooling_contract.functions.depositTokens(tokens).transact({'from': owner})
testerchain.wait_for_receipt(tx)
# Try to withdraw ETH twice
balance = testerchain.w3.eth.getBalance(contract_address)
transaction = pooling_contract.functions.withdrawETH().buildTransaction({'gas': 0})
tx = reentrancy_contract.functions.setData(1, transaction['to'], 0, transaction['data']).transact()
testerchain.wait_for_receipt(tx)
with pytest.raises((TransactionFailed, ValueError)):
tx = testerchain.client.send_transaction({'to': contract_address})
testerchain.wait_for_receipt(tx)
assert testerchain.w3.eth.getBalance(contract_address) == balance
# TODO same test for withdrawAll()