diff --git a/nucypher/blockchain/eth/sol/source/contracts/Issuer.sol b/nucypher/blockchain/eth/sol/source/contracts/Issuer.sol index a6a2c145e..9e2635105 100644 --- a/nucypher/blockchain/eth/sol/source/contracts/Issuer.sol +++ b/nucypher/blockchain/eth/sol/source/contracts/Issuer.sol @@ -166,6 +166,13 @@ contract Issuer is Upgradeable { currentSupply2 = currentSupply2.sub(_amount); } + /** + * @notice Returns the number of tokens that can be mined + **/ + function getReservedReward() public view returns (uint256) { + return totalSupply - Math.max256(currentSupply1, currentSupply2); + } + function verifyState(address _testTarget) public onlyOwner { require(address(uint160(delegateGet(_testTarget, "token()"))) == address(token)); require(delegateGet(_testTarget, "miningCoefficient()") == miningCoefficient); diff --git a/nucypher/blockchain/eth/sol/source/contracts/MinersEscrow.sol b/nucypher/blockchain/eth/sol/source/contracts/MinersEscrow.sol index 6d4f03c58..a562b0b6c 100644 --- a/nucypher/blockchain/eth/sol/source/contracts/MinersEscrow.sol +++ b/nucypher/blockchain/eth/sol/source/contracts/MinersEscrow.sol @@ -748,7 +748,10 @@ contract MinersEscrow is Issuer { } } - unMint(_penalty - _reward); + _penalty = _penalty - _reward; + if (_penalty > 0) { + unMint(_penalty); + } if (_reward > 0) { token.safeTransfer(_investigator, _reward); } @@ -784,9 +787,16 @@ contract MinersEscrow is Issuer { uint256 appliedPenalty = _penalty; if (_penalty < shortestSubStake.lockedValue) { shortestSubStake.lockedValue -= _penalty; + if (_info.confirmedPeriod1 != EMPTY_CONFIRMED_PERIOD && + _info.confirmedPeriod1 < _period || + _info.confirmedPeriod2 != EMPTY_CONFIRMED_PERIOD && + _info.confirmedPeriod2 < _period + ) { + saveSubStake(_info, shortestSubStake.firstPeriod, _period.sub16(1), 0, _penalty); + } _penalty = 0; } else { - shortestSubStake.lastPeriod = 1; + shortestSubStake.lastPeriod = _period.sub16(1); _penalty -= shortestSubStake.lockedValue; appliedPenalty = shortestSubStake.lockedValue; } diff --git a/tests/blockchain/eth/contracts/base/test_issuer.py b/tests/blockchain/eth/contracts/base/test_issuer.py index 2625738ec..ecc273186 100644 --- a/tests/blockchain/eth/contracts/base/test_issuer.py +++ b/tests/blockchain/eth/contracts/base/test_issuer.py @@ -104,6 +104,7 @@ def test_inflation_rate(testerchain, token): testerchain.wait_for_receipt(tx) tx = issuer.functions.initialize().transact({'from': creator}) testerchain.wait_for_receipt(tx) + reward = issuer.functions.getReservedReward().call() # Mint some tokens and save result of minting period = issuer.functions.getCurrentPeriod().call() @@ -115,11 +116,13 @@ def test_inflation_rate(testerchain, token): tx = issuer.functions.testMint(period + 1, 1, 1, 0).transact({'from': ursula}) testerchain.wait_for_receipt(tx) assert 2 * one_period == token.functions.balanceOf(ursula).call() + assert reward - token.functions.balanceOf(ursula).call() == issuer.functions.getReservedReward().call() # Mint tokens in the next period, inflation rate must be lower than in previous minting tx = issuer.functions.testMint(period + 2, 1, 1, 0).transact({'from': ursula}) testerchain.wait_for_receipt(tx) assert 3 * one_period > token.functions.balanceOf(ursula).call() + assert reward - token.functions.balanceOf(ursula).call() == issuer.functions.getReservedReward().call() minted_amount = token.functions.balanceOf(ursula).call() - 2 * one_period # Mint tokens in the first period again, inflation rate must be the same as in previous minting @@ -127,21 +130,26 @@ def test_inflation_rate(testerchain, token): tx = issuer.functions.testMint(period + 1, 1, 1, 0).transact({'from': ursula}) testerchain.wait_for_receipt(tx) assert 2 * one_period + 2 * minted_amount == token.functions.balanceOf(ursula).call() + assert reward - token.functions.balanceOf(ursula).call() == issuer.functions.getReservedReward().call() # Mint tokens in the next period, inflation rate must be lower than in previous minting tx = issuer.functions.testMint(period + 3, 1, 1, 0).transact({'from': ursula}) testerchain.wait_for_receipt(tx) assert 2 * one_period + 3 * minted_amount > token.functions.balanceOf(ursula).call() + assert reward - token.functions.balanceOf(ursula).call() == issuer.functions.getReservedReward().call() # Return some tokens as a reward balance = token.functions.balanceOf(ursula).call() + reward = issuer.functions.getReservedReward().call() tx = issuer.functions.testUnMint(2 * one_period + 2 * minted_amount).transact() testerchain.wait_for_receipt(tx) + assert reward + 2 * one_period + 2 * minted_amount == issuer.functions.getReservedReward().call() # Rate will be increased because some tokens were returned tx = issuer.functions.testMint(period + 3, 1, 1, 0).transact({'from': ursula}) testerchain.wait_for_receipt(tx) assert balance + one_period == token.functions.balanceOf(ursula).call() + assert reward + one_period + 2 * minted_amount == issuer.functions.getReservedReward().call() @pytest.mark.slow diff --git a/tests/blockchain/eth/contracts/main/miners_escrow/test_mining.py b/tests/blockchain/eth/contracts/main/miners_escrow/test_mining.py index e64cffce0..020206c72 100644 --- a/tests/blockchain/eth/contracts/main/miners_escrow/test_mining.py +++ b/tests/blockchain/eth/contracts/main/miners_escrow/test_mining.py @@ -331,7 +331,17 @@ def test_slashing(testerchain, token, escrow_contract): assert 100 == escrow.functions.lockedPerPeriod(period).call() assert 0 == escrow.functions.lockedPerPeriod(period + 1).call() + # Can't slash directly using the escrow contract + with pytest.raises((TransactionFailed, ValueError)): + tx = escrow.functions.slashMiner(ursula, 100, investigator, 10).transact() + testerchain.wait_for_receipt(tx) + # Penalty must be greater than zero + with pytest.raises((TransactionFailed, ValueError)): + tx = escrow.functions.slashMiner(ursula, 0, investigator, 0).transact() + testerchain.wait_for_receipt(tx) + # Slash the whole stake + reward = escrow.functions.getReservedReward().call() tx = overseer.functions.slashMiner(ursula, 100, investigator, 10).transact() testerchain.wait_for_receipt(tx) # Miner has no more sub stakes @@ -339,6 +349,7 @@ def test_slashing(testerchain, token, escrow_contract): assert 0 == escrow.functions.getLockedTokens(ursula).call() assert 10 == token.functions.balanceOf(investigator).call() assert 0 == escrow.functions.lockedPerPeriod(period).call() + assert reward + 90 == escrow.functions.getReservedReward().call() # New deposit and confirmation of activity tx = escrow.functions.deposit(100, 5).transact({'from': ursula}) @@ -353,6 +364,7 @@ def test_slashing(testerchain, token, escrow_contract): assert 100 == escrow.functions.lockedPerPeriod(period + 1).call() # Slash part of one sub stake (there is only one sub stake) + reward = escrow.functions.getReservedReward().call() tx = overseer.functions.slashMiner(ursula, 10, investigator, 11).transact() testerchain.wait_for_receipt(tx) assert 90 == escrow.functions.minerInfo(ursula).call()[VALUE_FIELD] @@ -361,6 +373,7 @@ def test_slashing(testerchain, token, escrow_contract): assert 20 == token.functions.balanceOf(investigator).call() assert 90 == escrow.functions.lockedPerPeriod(period).call() assert 90 == escrow.functions.lockedPerPeriod(period + 1).call() + assert reward == escrow.functions.getReservedReward().call() # New deposit of a longer sub stake tx = escrow.functions.deposit(100, 6).transact({'from': ursula}) @@ -376,6 +389,7 @@ def test_slashing(testerchain, token, escrow_contract): assert 0 == escrow.functions.lockedPerPeriod(period + 1).call() # Slash again part of the first sub stake because new sub stake is longer (there are two sub stakes) + reward = escrow.functions.getReservedReward().call() tx = overseer.functions.slashMiner(ursula, 10, investigator, 0).transact() testerchain.wait_for_receipt(tx) assert 180 == escrow.functions.minerInfo(ursula).call()[VALUE_FIELD] @@ -385,6 +399,7 @@ def test_slashing(testerchain, token, escrow_contract): assert 20 == token.functions.balanceOf(investigator).call() assert 90 == escrow.functions.lockedPerPeriod(period - 1).call() assert 180 == escrow.functions.lockedPerPeriod(period).call() + assert reward + 10 == escrow.functions.getReservedReward().call() # New deposit of a shorter sub stake tx = escrow.functions.deposit(110, 2).transact({'from': ursula}) @@ -399,6 +414,7 @@ def test_slashing(testerchain, token, escrow_contract): assert 0 == escrow.functions.lockedPerPeriod(period).call() # Slash only free amount of tokens + reward = escrow.functions.getReservedReward().call() tx = overseer.functions.slashMiner(ursula, deposit - 290, investigator, 0).transact() testerchain.wait_for_receipt(tx) assert 290 == escrow.functions.minerInfo(ursula).call()[VALUE_FIELD] @@ -408,6 +424,7 @@ def test_slashing(testerchain, token, escrow_contract): assert 20 == token.functions.balanceOf(investigator).call() assert 290 == escrow.functions.lockedPerPeriod(period - 1).call() assert 0 == escrow.functions.lockedPerPeriod(period).call() + assert reward + deposit - 290 == escrow.functions.getReservedReward().call() # Slash only the new sub stake because it's the shortest one (there are three sub stakes) tx = overseer.functions.slashMiner(ursula, 10, investigator, 0).transact() @@ -419,31 +436,31 @@ def test_slashing(testerchain, token, escrow_contract): assert 20 == token.functions.balanceOf(investigator).call() assert 290 == escrow.functions.lockedPerPeriod(period - 1).call() assert 0 == escrow.functions.lockedPerPeriod(period).call() + assert reward + deposit - 280 == escrow.functions.getReservedReward().call() # New deposit tx = escrow.functions.deposit(100, 2).transact({'from': ursula}) testerchain.wait_for_receipt(tx) - assert 290 == escrow.functions.getLockedTokens(ursula, -1).call() assert 280 == escrow.functions.getLockedTokens(ursula).call() assert 380 == escrow.functions.getLockedTokens(ursula, 1).call() assert 380 == escrow.functions.lockedPerPeriod(period + 1).call() deposit = escrow.functions.minerInfo(ursula).call()[VALUE_FIELD] # Some reward is already mined - reward = deposit - 380 + unlocked_deposit = deposit - 380 + reward = escrow.functions.getReservedReward().call() # Slash the new sub stake which starts in the next period # Because locked value is more in the next period than in the current period - tx = overseer.functions.slashMiner(ursula, reward + 10, investigator, 0).transact() + tx = overseer.functions.slashMiner(ursula, unlocked_deposit + 10, investigator, 0).transact() testerchain.wait_for_receipt(tx) - assert 290 == escrow.functions.getLockedTokens(ursula, -1).call() assert 280 == escrow.functions.getLockedTokens(ursula).call() assert 370 == escrow.functions.getLockedTokens(ursula, 1).call() assert 370 == escrow.functions.minerInfo(ursula).call()[VALUE_FIELD] assert 370 == escrow.functions.lockedPerPeriod(period + 1).call() + assert reward + unlocked_deposit + 10 == escrow.functions.getReservedReward().call() # After two periods shortest sub stake will be unlocked, lock again and slash after this testerchain.time_travel(hours=1) period += 1 - assert 280 == escrow.functions.getLockedTokens(ursula, -1).call() assert 370 == escrow.functions.getLockedTokens(ursula).call() assert 270 == escrow.functions.getLockedTokens(ursula, 1).call() assert 100 == escrow.functions.getLockedTokens(ursula, 3).call() @@ -452,7 +469,6 @@ def test_slashing(testerchain, token, escrow_contract): assert 0 == escrow.functions.lockedPerPeriod(period + 1).call() tx = escrow.functions.lock(100, 2).transact({'from': ursula}) testerchain.wait_for_receipt(tx) - assert 280 == escrow.functions.getLockedTokens(ursula, -1).call() assert 370 == escrow.functions.getLockedTokens(ursula).call() assert 370 == escrow.functions.getLockedTokens(ursula, 1).call() assert 100 == escrow.functions.getLockedTokens(ursula, 3).call() @@ -463,9 +479,9 @@ def test_slashing(testerchain, token, escrow_contract): # Slash two sub stakes: # one which will be unlocked after current period and new sub stake + reward = escrow.functions.getReservedReward().call() tx = overseer.functions.slashMiner(ursula, 10, investigator, 0).transact() testerchain.wait_for_receipt(tx) - assert 280 == escrow.functions.getLockedTokens(ursula, -1).call() assert 360 == escrow.functions.getLockedTokens(ursula).call() assert 360 == escrow.functions.getLockedTokens(ursula, 1).call() assert 100 == escrow.functions.getLockedTokens(ursula, 3).call() @@ -473,12 +489,12 @@ def test_slashing(testerchain, token, escrow_contract): assert 0 == escrow.functions.lockedPerPeriod(period - 1).call() assert 360 == escrow.functions.lockedPerPeriod(period).call() assert 360 == escrow.functions.lockedPerPeriod(period + 1).call() + assert reward + 10 == escrow.functions.getReservedReward().call() # Slash three sub stakes: # one which will be unlocked after current period, new sub stake and another short sub stake tx = overseer.functions.slashMiner(ursula, 90, investigator, 0).transact() testerchain.wait_for_receipt(tx) - assert 280 == escrow.functions.getLockedTokens(ursula, -1).call() assert 270 == escrow.functions.getLockedTokens(ursula).call() assert 270 == escrow.functions.getLockedTokens(ursula, 1).call() assert 100 == escrow.functions.getLockedTokens(ursula, 3).call() @@ -486,3 +502,4 @@ def test_slashing(testerchain, token, escrow_contract): assert 0 == escrow.functions.lockedPerPeriod(period - 1).call() assert 270 == escrow.functions.lockedPerPeriod(period).call() assert 270 == escrow.functions.lockedPerPeriod(period + 1).call() + assert reward + 100 == escrow.functions.getReservedReward().call()