Tests balance snapshots in StakingEscrow

Note that we're even testing the actual storage content
pull/1917/head
David Núñez 2020-04-26 04:31:59 +02:00
parent 94a2480416
commit 44b8127407
1 changed files with 313 additions and 1 deletions

View File

@ -15,14 +15,17 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from bisect import bisect
import pytest
from eth_tester.exceptions import TransactionFailed
from eth_utils import keccak
from eth_utils import to_canonical_address
from web3 import Web3
from web3.contract import Contract
from nucypher.blockchain.eth.constants import NULL_ADDRESS
from nucypher.blockchain.eth.token import NU
from tests.utils.solidity import to_bytes32, get_mapping_entry_location, get_array_data_location
LOCK_RE_STAKE_UNTIL_PERIOD_FIELD = 4
@ -1085,3 +1088,312 @@ def test_wind_down(testerchain, token, escrow_contract, token_economics):
tx = escrow.functions.setWindDown(False).transact({'from': staker})
testerchain.wait_for_receipt(tx)
check_last_period()
@pytest.mark.slow
def test_snapshots(testerchain, token, escrow_contract):
# HELPER FUNCTIONS #
class TestSnapshot:
"""Mimics how our Snapshots library work in a contract"""
def __init__(self):
self.history = {0: 0}
self.timestamps = [0]
def add_value_at(self, time, value):
if self.timestamps[-1] == time:
self.history[time] = value
return
elif time < self.timestamps[-1]:
assert False
self.timestamps.append(time)
self.history[time] = value
def add_value(self, value):
self.add_value_at(testerchain.get_block_number(), value)
def last_value(self):
return self.history[self.timestamps[-1]]
def get_value_at(self, time):
if time > self.timestamps[-1]:
return self.last_value()
else:
return self.history[self.timestamps[bisect(self.timestamps, time) - 1]]
@classmethod
def from_list(cls, snapshots):
s = cls()
for t, v in snapshots:
s.timestamps.append(t)
s.history[t] = v
return s
def __str__(self):
return str(self.history)
def __eq__(self, other):
return self.history == other.history and self.timestamps == other.timestamps
def staker_has_snapshots_enabled(staker) -> bool:
_, _, _, snapshots_enabled = escrow.functions.getFlags(staker).call()
return snapshots_enabled
def decode_snapshots_from_slot(slot):
slot = to_bytes32(slot)
snapshot_2 = Web3.toInt(slot[:4]), Web3.toInt(slot[4:16])
snapshot_1 = Web3.toInt(slot[16:20]), Web3.toInt(slot[20:32])
return snapshot_1, snapshot_2
def get_staker_history_from_storage(staker):
STAKERS_MAPPING_SLOT = 6
HISTORY_SLOT_IN_STAKER_INFO = 12
# See https://solidity.readthedocs.io/en/latest/internals/layout_in_storage.html#mappings-and-dynamic-arrays
staker_location = get_mapping_entry_location(key=to_bytes32(hexstr=staker),
mapping_location=STAKERS_MAPPING_SLOT)
length_position = staker_location + HISTORY_SLOT_IN_STAKER_INFO
data_position = get_array_data_location(length_position)
length = testerchain.read_storage_slot(escrow.address, length_position)
length_in_slots = (length + 1)//2
slots = [testerchain.read_storage_slot(escrow.address, data_position + i) for i in range(length_in_slots)]
snapshots = list()
for snapshot_1, snapshot_2 in map(decode_snapshots_from_slot, slots):
snapshots.append(snapshot_1)
if snapshot_2 != (0, 0):
snapshots.append(snapshot_2)
return TestSnapshot.from_list(snapshots)
def get_global_history_from_storage():
GLOBAL_HISTORY_SLOT_IN_CONTRACT = 10
# See https://solidity.readthedocs.io/en/latest/internals/layout_in_storage.html#mappings-and-dynamic-arrays
length = testerchain.read_storage_slot(escrow.address, GLOBAL_HISTORY_SLOT_IN_CONTRACT)
snapshots = list()
for i in range(length):
snapshot_bytes = Web3.toBytes(escrow.functions.balanceHistory(i).call()).rjust(16, b'\0')
snapshots.append((Web3.toInt(snapshot_bytes[:4]), Web3.toInt(snapshot_bytes[4:16])))
return TestSnapshot.from_list(snapshots)
#
# TEST STARTS HERE #
#
escrow = escrow_contract(10000)
creator = testerchain.client.accounts[0]
staker1 = testerchain.client.accounts[1]
staker2 = testerchain.client.accounts[2]
snapshot_log = escrow.events.SnapshotSet.createFilter(fromBlock='latest')
# Give Escrow tokens for reward and initialize contract
reward = 10 ** 9
tx = token.functions.approve(escrow.address, reward).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = escrow.functions.initialize(reward).transact({'from': creator})
testerchain.wait_for_receipt(tx)
expected_staker1_balance = TestSnapshot()
expected_staker2_balance = TestSnapshot()
expected_global_balance = TestSnapshot()
assert expected_staker1_balance == get_staker_history_from_storage(staker1)
assert expected_staker2_balance == get_staker_history_from_storage(staker2)
assert expected_global_balance == get_global_history_from_storage()
# Set snapshot parameter even before initialization. Disabling snapshots always creates a new snapshot with value 0
assert staker_has_snapshots_enabled(staker1)
tx = escrow.functions.setSnapshots(False).transact({'from': staker1})
testerchain.wait_for_receipt(tx)
assert not staker_has_snapshots_enabled(staker1)
expected_staker1_balance.add_value(0)
expected_global_balance.add_value(0)
assert expected_staker1_balance == get_staker_history_from_storage(staker1)
assert expected_global_balance == get_global_history_from_storage()
# Activating the snapshots again will create a new snapshot with current balance, which is 0
tx = escrow.functions.setSnapshots(True).transact({'from': staker1})
testerchain.wait_for_receipt(tx)
assert staker_has_snapshots_enabled(staker1)
expected_staker1_balance.add_value(0)
expected_global_balance.add_value(0)
assert expected_staker1_balance == get_staker_history_from_storage(staker1)
assert expected_global_balance == get_global_history_from_storage()
# Check emitted events
events = snapshot_log.get_all_entries()
assert 2 == len(events)
event_args = events[0]['args']
assert staker1 == event_args['staker']
assert not event_args['snapshotsEnabled']
event_args = events[1]['args']
assert staker1 == event_args['staker']
assert event_args['snapshotsEnabled']
# Staker disables restaking, deposits some tokens and confirms activity
_wind_down, re_stake, _measure_work, _snapshots = escrow.functions.getFlags(staker1).call()
assert re_stake
tx = escrow.functions.setReStake(False).transact({'from': staker1})
testerchain.wait_for_receipt(tx)
_wind_down, re_stake, _measure_work, _snapshots = escrow.functions.getFlags(staker1).call()
assert not re_stake
tx = token.functions.transfer(staker1, 10000).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = token.functions.approve(escrow.address, 10000).transact({'from': staker1})
testerchain.wait_for_receipt(tx)
initial_deposit = 100
tx = escrow.functions.deposit(initial_deposit, 10).transact({'from': staker1})
testerchain.wait_for_receipt(tx)
expected_staker1_balance.add_value(initial_deposit)
expected_global_balance.add_value(initial_deposit)
assert expected_staker1_balance == get_staker_history_from_storage(staker1)
assert expected_global_balance == get_global_history_from_storage()
now = testerchain.get_block_number()
assert escrow.functions.totalStakedForAt(staker1, now).call() == expected_staker1_balance.get_value_at(now)
assert escrow.functions.totalStakedAt(now).call() == expected_global_balance.get_value_at(now)
# Set worker doesn't affect snapshots
tx = escrow.functions.setWorker(staker1).transact({'from': staker1})
testerchain.wait_for_receipt(tx)
assert expected_staker1_balance == get_staker_history_from_storage(staker1)
assert expected_global_balance == get_global_history_from_storage()
# Now that we do have a positive balance, let's deactivate snapshots and check them
tx = escrow.functions.setSnapshots(False).transact({'from': staker1})
testerchain.wait_for_receipt(tx)
assert not staker_has_snapshots_enabled(staker1)
expected_staker1_balance.add_value(0)
expected_global_balance.add_value(0)
assert expected_staker1_balance == get_staker_history_from_storage(staker1)
assert expected_global_balance == get_global_history_from_storage()
assert initial_deposit == escrow.functions.getAllTokens(staker1).call()
now = testerchain.get_block_number()
assert 0 == escrow.functions.totalStakedForAt(staker1, now).call()
assert 0 == escrow.functions.totalStakedAt(now).call()
assert initial_deposit == escrow.functions.totalStakedForAt(staker1, now - 1).call()
assert initial_deposit == escrow.functions.totalStakedAt(now - 1).call()
# Activating the snapshots again will create a new snapshot with current balance (100)
tx = escrow.functions.setSnapshots(True).transact({'from': staker1})
testerchain.wait_for_receipt(tx)
assert staker_has_snapshots_enabled(staker1)
expected_staker1_balance.add_value(initial_deposit)
expected_global_balance.add_value(initial_deposit)
assert expected_staker1_balance == get_staker_history_from_storage(staker1)
assert expected_global_balance == get_global_history_from_storage()
now = testerchain.get_block_number()
assert initial_deposit == escrow.functions.totalStakedForAt(staker1, now).call()
assert initial_deposit == escrow.functions.totalStakedAt(now).call()
assert 0 == escrow.functions.totalStakedForAt(staker1, now - 1).call()
assert 0 == escrow.functions.totalStakedAt(now - 1).call()
# First confirm activity doesn't affect balance
tx = escrow.functions.confirmActivity().transact({'from': staker1})
testerchain.wait_for_receipt(tx)
assert expected_staker1_balance == get_staker_history_from_storage(staker1)
assert expected_global_balance == get_global_history_from_storage()
testerchain.time_travel(hours=1)
assert now < testerchain.get_block_number()
now = testerchain.get_block_number()
assert initial_deposit == escrow.functions.totalStakedForAt(staker1, now).call()
assert initial_deposit == escrow.functions.totalStakedAt(now).call()
assert initial_deposit == escrow.functions.getAllTokens(staker1).call()
assert expected_staker1_balance == get_staker_history_from_storage(staker1)
assert expected_global_balance == get_global_history_from_storage()
# 2nd confirm activity, still no change in balance
tx = escrow.functions.confirmActivity().transact({'from': staker1})
testerchain.wait_for_receipt(tx)
testerchain.time_travel(hours=1)
assert now < testerchain.get_block_number()
now = testerchain.get_block_number()
assert initial_deposit == escrow.functions.totalStakedForAt(staker1, now).call()
assert initial_deposit == escrow.functions.totalStakedAt(now).call()
assert initial_deposit == escrow.functions.getAllTokens(staker1).call()
assert expected_staker1_balance == get_staker_history_from_storage(staker1)
assert expected_global_balance == get_global_history_from_storage()
# Minting tokens should increase balance
tx = escrow.functions.mint().transact({'from': staker1})
testerchain.wait_for_receipt(tx)
balance_staker1 = escrow.functions.getAllTokens(staker1).call()
assert balance_staker1 > initial_deposit
expected_staker1_balance.add_value(balance_staker1)
expected_global_balance.add_value(balance_staker1)
now = testerchain.get_block_number()
assert balance_staker1 == escrow.functions.getAllTokens(staker1).call()
assert balance_staker1 == escrow.functions.totalStakedForAt(staker1, now).call()
assert balance_staker1 == escrow.functions.totalStakedAt(now).call()
assert expected_staker1_balance == get_staker_history_from_storage(staker1)
assert expected_global_balance == get_global_history_from_storage()
# A SECOND STAKER APPEARS:
# Disable snapshots even before initialization. This creates a new snapshot with value 0
assert staker_has_snapshots_enabled(staker2)
tx = escrow.functions.setSnapshots(False).transact({'from': staker2})
testerchain.wait_for_receipt(tx)
assert not staker_has_snapshots_enabled(staker2)
expected_staker2_balance.add_value(0)
expected_global_balance.add_value(balance_staker1)
assert expected_staker2_balance == get_staker_history_from_storage(staker2)
assert expected_global_balance == get_global_history_from_storage()
# Staker 2 deposits some tokens and confirms activity. Since snapshots are disabled, no changes in history
tx = token.functions.transfer(staker2, 10000).transact({'from': creator})
testerchain.wait_for_receipt(tx)
tx = token.functions.approve(escrow.address, 10000).transact({'from': staker2})
testerchain.wait_for_receipt(tx)
deposit_staker2 = 100
tx = escrow.functions.deposit(deposit_staker2, 10).transact({'from': staker2})
testerchain.wait_for_receipt(tx)
assert deposit_staker2 == escrow.functions.getAllTokens(staker2).call()
assert expected_staker2_balance == get_staker_history_from_storage(staker2)
assert expected_global_balance == get_global_history_from_storage()
tx = escrow.functions.setWorker(staker2).transact({'from': staker2})
testerchain.wait_for_receipt(tx)
# Now that we do have a positive balance, let's activate snapshots and check them
tx = escrow.functions.setSnapshots(True).transact({'from': staker2})
testerchain.wait_for_receipt(tx)
assert staker_has_snapshots_enabled(staker2)
expected_staker2_balance.add_value(deposit_staker2)
expected_global_balance.add_value(balance_staker1 + deposit_staker2)
assert expected_staker2_balance == get_staker_history_from_storage(staker2)
assert expected_global_balance == get_global_history_from_storage()
now = testerchain.get_block_number()
assert deposit_staker2 == escrow.functions.totalStakedForAt(staker2, now).call()
assert deposit_staker2 + balance_staker1 == escrow.functions.totalStakedAt(now).call()
assert 0 == escrow.functions.totalStakedForAt(staker2, now - 1).call()
assert balance_staker1 == escrow.functions.totalStakedAt(now - 1).call()
# Finally, the first staker withdraws some tokens
withdrawal = 42
tx = escrow.functions.withdraw(withdrawal).transact({'from': staker1})
testerchain.wait_for_receipt(tx)
last_balance_staker1 = balance_staker1 - withdrawal
assert last_balance_staker1 == escrow.functions.getAllTokens(staker1).call()
expected_staker1_balance.add_value(last_balance_staker1)
expected_global_balance.add_value(last_balance_staker1 + deposit_staker2)
assert expected_staker1_balance == get_staker_history_from_storage(staker1)
assert expected_global_balance == get_global_history_from_storage()
now = testerchain.get_block_number()
assert last_balance_staker1 == escrow.functions.totalStakedForAt(staker1, now).call()
assert last_balance_staker1 + deposit_staker2 == escrow.functions.totalStakedAt(now).call()
assert balance_staker1 == escrow.functions.totalStakedForAt(staker1, now - 1).call()
assert balance_staker1 + deposit_staker2 == escrow.functions.totalStakedAt(now - 1).call()