From f234ba383f7ad34633c3d035ab54fd9af87d62a9 Mon Sep 17 00:00:00 2001 From: George Avsetsin Date: Thu, 1 Feb 2024 13:21:31 +0300 Subject: [PATCH] simple dvt rewards distribution tests --- configs/config_mainnet.py | 7 + interfaces/0xsplits/SplitWallet.json | 1 + interfaces/obol_split/ObolLidoSplit.json | 1 + .../obol_split/ObolLidoSplitFactory.json | 1 + .../test_sdvt_rewards_happy_path.py | 149 ++++++++++++++++++ ...test_sdvt_splitter_rewards_distribution.py | 138 ++++++++++++++++ utils/config.py | 8 + utils/test/reward_wrapper_helpers.py | 58 +++++++ utils/test/simple_dvt_helpers.py | 32 ++++ utils/test/split_helpers.py | 114 ++++++++++++++ utils/test/staking_router_helpers.py | 17 ++ 11 files changed, 526 insertions(+) create mode 100644 interfaces/0xsplits/SplitWallet.json create mode 100644 interfaces/obol_split/ObolLidoSplit.json create mode 100644 interfaces/obol_split/ObolLidoSplitFactory.json create mode 100644 tests/regression/test_sdvt_rewards_happy_path.py create mode 100644 tests/regression/test_sdvt_splitter_rewards_distribution.py create mode 100644 utils/test/reward_wrapper_helpers.py create mode 100644 utils/test/split_helpers.py create mode 100644 utils/test/staking_router_helpers.py diff --git a/configs/config_mainnet.py b/configs/config_mainnet.py index efbddf2c..6e7d5749 100644 --- a/configs/config_mainnet.py +++ b/configs/config_mainnet.py @@ -269,3 +269,10 @@ # Anchor ANCHOR_VAULT_PROXY = "0xA2F987A546D4CD1c607Ee8141276876C26b72Bdf" + +# 0xSplits +SPLIT_MAIN = "0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE" + +# Rewards Wrapper (aka ObolLidoSplit) +OBOL_LIDO_SPLIT_FACTORY = "0xA9d94139A310150Ca1163b5E23f3E1dbb7D9E2A6" +OBOL_LIDO_SPLIT_IMPL = "0x2fB59065F049e0D0E3180C6312FA0FeB5Bbf0FE3" diff --git a/interfaces/0xsplits/SplitWallet.json b/interfaces/0xsplits/SplitWallet.json new file mode 100644 index 00000000..26a54110 --- /dev/null +++ b/interfaces/0xsplits/SplitWallet.json @@ -0,0 +1 @@ +[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"Unauthorized","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"split","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ReceiveETH","type":"event"},{"inputs":[{"internalType":"contract ERC20","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"sendERC20ToMain","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"sendETHToMain","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"splitMain","outputs":[{"internalType":"contract ISplitMain","name":"","type":"address"}],"stateMutability":"view","type":"function"}] diff --git a/interfaces/obol_split/ObolLidoSplit.json b/interfaces/obol_split/ObolLidoSplit.json new file mode 100644 index 00000000..f28b0e7a --- /dev/null +++ b/interfaces/obol_split/ObolLidoSplit.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_feeRecipient","type":"address"},{"internalType":"uint256","name":"_feeShare","type":"uint256"},{"internalType":"contract ERC20","name":"_stETH","type":"address"},{"internalType":"contract ERC20","name":"_wstETH","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"Invalid_Address","type":"error"},{"inputs":[],"name":"Invalid_FeeRecipient","type":"error"},{"inputs":[{"internalType":"uint256","name":"fee","type":"uint256"}],"name":"Invalid_FeeShare","type":"error"},{"inputs":[],"name":"distribute","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"feeRecipient","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feeShare","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"rescueFunds","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"splitWallet","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"stETH","outputs":[{"internalType":"contract ERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"wstETH","outputs":[{"internalType":"contract ERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"}] diff --git a/interfaces/obol_split/ObolLidoSplitFactory.json b/interfaces/obol_split/ObolLidoSplitFactory.json new file mode 100644 index 00000000..c2f423bd --- /dev/null +++ b/interfaces/obol_split/ObolLidoSplitFactory.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_feeRecipient","type":"address"},{"internalType":"uint256","name":"_feeShare","type":"uint256"},{"internalType":"contract ERC20","name":"_stETH","type":"address"},{"internalType":"contract ERC20","name":"_wstETH","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"Invalid_Wallet","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"split","type":"address"}],"name":"CreateObolLidoSplit","type":"event"},{"inputs":[{"internalType":"address","name":"splitWallet","type":"address"}],"name":"createSplit","outputs":[{"internalType":"address","name":"lidoSplit","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"lidoSplitImpl","outputs":[{"internalType":"contract ObolLidoSplit","name":"","type":"address"}],"stateMutability":"view","type":"function"}] diff --git a/tests/regression/test_sdvt_rewards_happy_path.py b/tests/regression/test_sdvt_rewards_happy_path.py new file mode 100644 index 00000000..6abc1197 --- /dev/null +++ b/tests/regression/test_sdvt_rewards_happy_path.py @@ -0,0 +1,149 @@ +import pytest + +from brownie import accounts, ZERO_ADDRESS +from utils.test.helpers import ETH +from utils.config import contracts + +from utils.test.reward_wrapper_helpers import deploy_reward_wrapper, wrap_and_split_rewards +from utils.test.split_helpers import ( + deploy_split_wallet, + get_split_percent_allocation, + get_split_percentage_scale, + split_and_withdraw_wsteth_rewards, +) +from utils.test.simple_dvt_helpers import simple_dvt_add_keys, simple_dvt_vet_keys, simple_dvt_add_node_operators +from utils.test.staking_router_helpers import pause_staking_module +from utils.test.oracle_report_helpers import oracle_report + + +WEI_TOLERANCE = 5 # wei tolerance to avoid rounding issue + + +# fixtures + + +@pytest.fixture(scope="module") +def cluster_participants(accounts): + CLUSTER_PARTICIPANTS = 5 + + return sorted(map(lambda participant: participant.address, accounts[0:CLUSTER_PARTICIPANTS])) + + +@pytest.fixture(scope="module") +def split_wallet(cluster_participants): + percentage_scale = get_split_percentage_scale() + percent_allocation = get_split_percent_allocation(len(cluster_participants), percentage_scale) + (deployed_contract, _) = deploy_split_wallet(cluster_participants, percent_allocation, cluster_participants[0]) + + return deployed_contract + + +@pytest.fixture(scope="module") +def reward_wrapper(split_wallet, cluster_participants): + (deployed_contract, _) = deploy_reward_wrapper(split_wallet, cluster_participants[0]) + return deployed_contract + + +@pytest.fixture(scope="function") +def simple_dvt_module_id(): + modules = contracts.staking_router.getStakingModules() + return next(filter(lambda module: module[1] == contracts.simple_dvt.address, modules))[0] + + +# staking router <> simple dvt tests + + +def test_sdvt_module_connected_to_router(): + """ + Test that simple dvt module is connected to staking router + """ + modules = contracts.staking_router.getStakingModules() + assert any(map(lambda module: module[1] == contracts.simple_dvt.address, modules)) + + +# full happy path test + + +def test_rewards_distribution_happy_path(simple_dvt_module_id, cluster_participants, reward_wrapper): + """ + Test happy path of rewards distribution + Test adding new cluster to simple dvt module, depositing to simple dvt module, distributing and claiming rewards + """ + simple_dvt, staking_router = contracts.simple_dvt, contracts.staking_router + lido, deposit_security_module = contracts.lido, contracts.deposit_security_module + withdrawal_queue = contracts.withdrawal_queue + + stranger = cluster_participants[0] + + new_cluster_name = "new cluster" + new_manager_address = "0x1110000000000000000000000000000011111111" + new_reward_address = reward_wrapper.address + + # add operator to simple dvt module + input_params = [(new_cluster_name, new_reward_address, new_manager_address)] + (node_operators_count_before, node_operator_count_after) = simple_dvt_add_node_operators( + simple_dvt, stranger, input_params + ) + operator_id = node_operator_count_after - 1 + assert node_operator_count_after == node_operators_count_before + len(input_params) + + # add keys to the operator + simple_dvt_add_keys(simple_dvt, operator_id, 10) + + # vet operator keys + simple_dvt_vet_keys(operator_id, stranger) + + # pause deposit to all modules except simple dvt + # to be sure that all deposits go to simple dvt + modules = staking_router.getStakingModules() + for module in modules: + if module[0] != simple_dvt_module_id: + pause_staking_module(module[0]) + + # fill the deposit buffer + deposits_count = 10 + deposit_size = ETH(32) + + buffered_ether_before_submit = lido.getBufferedEther() + withdrawal_unfinalized_steth = withdrawal_queue.unfinalizedStETH() + + required_buffer = max(0, withdrawal_unfinalized_steth - buffered_ether_before_submit) + required_buffer += deposits_count * deposit_size + WEI_TOLERANCE + + eth_whale = accounts.at(staking_router.DEPOSIT_CONTRACT(), force=True) + lido.submit(ZERO_ADDRESS, {"from": eth_whale, "value": required_buffer}) + + assert lido.getDepositableEther() >= required_buffer + + # deposit to simple dvt + module_summary_before = staking_router.getStakingModuleSummary(simple_dvt_module_id) + lido.deposit(deposits_count, simple_dvt_module_id, "0x", {"from": deposit_security_module}) + module_summary_after = staking_router.getStakingModuleSummary(simple_dvt_module_id) + + assert ( + module_summary_after["totalDepositedValidators"] + == module_summary_before["totalDepositedValidators"] + deposits_count + ) + + # check that there is no steth on the cluster reward address + cluster_rewards_before_report = lido.balanceOf(new_reward_address) + assert cluster_rewards_before_report == 0 + + # oracle report + oracle_report(cl_diff=ETH(100)) + cluster_rewards_after_report = lido.balanceOf(new_reward_address) + + # check that cluster reward address balance increased + assert cluster_rewards_after_report > 0 + + # wrap rewards and split between dvt provider and split wallet + wrap_and_split_rewards(reward_wrapper, stranger) + + # split wsteth rewards between participants and withdraw + split_and_withdraw_wsteth_rewards( + reward_wrapper.splitWallet(), + cluster_participants, + get_split_percent_allocation(len(cluster_participants), get_split_percentage_scale()), + get_split_percentage_scale(), + stranger, + ) diff --git a/tests/regression/test_sdvt_splitter_rewards_distribution.py b/tests/regression/test_sdvt_splitter_rewards_distribution.py new file mode 100644 index 00000000..d06eb133 --- /dev/null +++ b/tests/regression/test_sdvt_splitter_rewards_distribution.py @@ -0,0 +1,138 @@ +import pytest + +from brownie import ZERO_ADDRESS +from utils.config import contracts + +from utils.test.reward_wrapper_helpers import deploy_reward_wrapper, wrap_and_split_rewards +from utils.test.split_helpers import ( + deploy_split_wallet, + get_split_percent_allocation, + get_split_percentage_scale, + split_and_withdraw_wsteth_rewards, +) + +WEI_TOLERANCE = 5 # wei tolerance to avoid rounding issue + + +# fixtures + + +@pytest.fixture(scope="module") +def cluster_participants(accounts): + CLUSTER_PARTICIPANTS = 5 + + return sorted(map(lambda participant: participant.address, accounts[0:CLUSTER_PARTICIPANTS])) + + +@pytest.fixture(scope="module") +def split_percentage_scale(): + return get_split_percentage_scale() + + +@pytest.fixture(scope="module") +def split_percent_allocation(cluster_participants, split_percentage_scale): + return get_split_percent_allocation(len(cluster_participants), split_percentage_scale) + + +@pytest.fixture(scope="module") +def split_wallet(cluster_participants, split_percent_allocation): + (deployed_contract, _) = deploy_split_wallet( + cluster_participants, split_percent_allocation, cluster_participants[0] + ) + + return deployed_contract + + +@pytest.fixture(scope="module") +def reward_wrapper(split_wallet, cluster_participants): + (deployed_contract, _) = deploy_reward_wrapper(split_wallet, cluster_participants[0]) + + return deployed_contract + + +def test_reward_wrapper_deploy(reward_wrapper, split_wallet): + """ + Test reward wrapper contract deployment + """ + connected_split_wallet = reward_wrapper.splitWallet() + assert connected_split_wallet == split_wallet.address + + steth = reward_wrapper.stETH() + assert steth == contracts.lido.address + + wsteth = reward_wrapper.wstETH() + assert wsteth == contracts.wsteth.address + + fee_share = reward_wrapper.feeShare() + fee_recipient = reward_wrapper.feeRecipient() + + with_fee = fee_share > 0 and fee_recipient != ZERO_ADDRESS + without_fee = fee_share == 0 and fee_recipient == ZERO_ADDRESS + + assert with_fee or without_fee + + +def test_split_wallet_deploy(split_wallet): + """ + Test split wallet contract deployment + """ + assert split_wallet.splitMain() == contracts.split_main.address + + +# rewards wrapping tests + + +def test_wrap_rewards(accounts, reward_wrapper): + """ + Test rewards wrapping logic + Should wrap steth rewards to wsteth and split between dvt provider and split wallet + """ + steth = contracts.lido + steth_to_distribute = 1 * 10 ** contracts.lido.decimals() + stranger = accounts[0] + + # get steth to distribute + eth_to_submit = steth_to_distribute + WEI_TOLERANCE + steth.submit(ZERO_ADDRESS, {"from": stranger, "value": eth_to_submit}) + assert steth.balanceOf(stranger) >= steth_to_distribute + + # transfer steth to wrapper contract + assert steth.balanceOf(reward_wrapper.address) == 0 + steth.transfer(reward_wrapper.address, steth_to_distribute, {"from": stranger}) + assert steth.balanceOf(reward_wrapper.address) >= steth_to_distribute - WEI_TOLERANCE + + # wrap rewards and split between dvt provider and split wallet + wrap_and_split_rewards(reward_wrapper, stranger) + + +def test_split_rewards(accounts, split_wallet, cluster_participants, split_percent_allocation, split_percentage_scale): + """ + Test separate split wallet (instance of 0xSplit protocol) contract distribution logic + Should distribute wsteth rewards between participants according to split wallet shares + """ + wsteth = contracts.wsteth + stranger = accounts[0] + + wsteth_to_distribute = 1 * 10 ** contracts.wsteth.decimals() + + # check split wallet balance initial state + split_wallet_balance_before = wsteth.balanceOf(split_wallet) + assert split_wallet_balance_before == 0 + + # get required wsteth + eth_to_submit = wsteth.getStETHByWstETH(wsteth_to_distribute) + WEI_TOLERANCE + stranger.transfer(wsteth.address, eth_to_submit) + assert wsteth.balanceOf(stranger) >= wsteth_to_distribute + + # transfer wsteth to split wallet contract + wsteth.transfer(split_wallet.address, wsteth_to_distribute, {"from": stranger}) + assert wsteth.balanceOf(split_wallet.address) == wsteth_to_distribute + + # split wsteth rewards between participants and withdraw + split_and_withdraw_wsteth_rewards( + split_wallet.address, + cluster_participants, + split_percent_allocation, + split_percentage_scale, + stranger, + ) diff --git a/utils/config.py b/utils/config.py index 270fbf11..259fc658 100644 --- a/utils/config.py +++ b/utils/config.py @@ -268,6 +268,14 @@ def anchor_vault(self) -> interface.InsuranceFund: def anchor_vault_proxy(self) -> interface.InsuranceFund: return interface.AnchorVaultProxy(ANCHOR_VAULT_PROXY) + @property + def obol_lido_split_factory(self) -> interface.ObolLidoSplitFactory: + return interface.ObolLidoSplitFactory(OBOL_LIDO_SPLIT_FACTORY) + + @property + def split_main(self) -> interface.SplitMain: + return interface.SplitMain(SPLIT_MAIN) + def __getattr__(name: str) -> Any: if name == "contracts": diff --git a/utils/test/reward_wrapper_helpers.py b/utils/test/reward_wrapper_helpers.py new file mode 100644 index 00000000..0a6345b8 --- /dev/null +++ b/utils/test/reward_wrapper_helpers.py @@ -0,0 +1,58 @@ +from utils.config import contracts +from brownie import interface, ZERO_ADDRESS + +WEI_TOLERANCE = 5 # wei tolerance to avoid rounding issue + + +def deploy_reward_wrapper(split_wallet, deployer): + factory = contracts.obol_lido_split_factory + deploy_tx = factory.createSplit(split_wallet, {"from": deployer}) + + deployed_instance_address = deploy_tx.events["CreateObolLidoSplit"]["split"] + deployed_contract = interface.ObolLidoSplit(deployed_instance_address) + + return (deployed_contract, deploy_tx) + + +def wrap_and_split_rewards(reward_wrapper, stranger): + WRAPPER_FEE_PERCENTAGE_SCALE = 10**5 # dvt provider fee percentage scale + + steth, wsteth = contracts.lido, contracts.wsteth + split_wallet = reward_wrapper.splitWallet() + + # dvt provider fee variables + dvt_provider_fee = reward_wrapper.feeShare() + dvt_provider_fee_recipient = reward_wrapper.feeRecipient() + + # check initial contract balance + reward_wrapper_balance_before = steth.balanceOf(reward_wrapper) + + steth_to_distribute = reward_wrapper_balance_before + wsteth_to_distribute = wsteth.getWstETHByStETH(steth_to_distribute) + + assert steth_to_distribute > WEI_TOLERANCE, "no steth to distribute" + assert wsteth_to_distribute > WEI_TOLERANCE, "no wsteth to distribute" + + # get split wallet balance before distribution + split_wallet_wsteth_balance_before = wsteth.balanceOf(split_wallet) + + # distribute wrapped rewards and fee + dvt_provider_wsteth_balance_before = wsteth.balanceOf(dvt_provider_fee_recipient) + + reward_wrapper.distribute({"from": stranger}) + split_wallet_wsteth_balance_after = wsteth.balanceOf(split_wallet) + dvt_provider_wsteth_balance_after = wsteth.balanceOf(dvt_provider_fee_recipient) + + # check wrapper balance after distribution + assert steth.balanceOf(reward_wrapper.address) < WEI_TOLERANCE + + # check fee charged to dvt provider + expected_fee_charged = wsteth_to_distribute * dvt_provider_fee // WRAPPER_FEE_PERCENTAGE_SCALE + dvt_provider_expected_wsteth_balance = dvt_provider_wsteth_balance_before + expected_fee_charged + assert dvt_provider_wsteth_balance_after - dvt_provider_expected_wsteth_balance <= WEI_TOLERANCE + + # check split wallet balance after distribution + split_wallet_expected_wsteth_balance = ( + split_wallet_wsteth_balance_before + wsteth_to_distribute - expected_fee_charged + ) + assert split_wallet_wsteth_balance_after - split_wallet_expected_wsteth_balance <= WEI_TOLERANCE diff --git a/utils/test/simple_dvt_helpers.py b/utils/test/simple_dvt_helpers.py index 8d4a2b18..222de7b9 100644 --- a/utils/test/simple_dvt_helpers.py +++ b/utils/test/simple_dvt_helpers.py @@ -94,6 +94,38 @@ def fill_simple_dvt_ops_vetted_keys(stranger, min_ops_cnt=MIN_OPS_CNT, min_keys_ assert no["totalVettedValidators"] == no["totalAddedValidators"] +def simple_dvt_vet_keys(operator_id, stranger): + factory = interface.SetVettedValidatorsLimits(EASYTRACK_SIMPLE_DVT_SET_VETTED_VALIDATORS_LIMITS_FACTORY) + trusted_caller = accounts.at(EASYTRACK_SIMPLE_DVT_TRUSTED_CALLER, force=True) + + simple_dvt, easy_track = contracts.simple_dvt, contracts.easy_track + + operator = simple_dvt.getNodeOperator(operator_id, False) + + if operator["totalVettedValidators"] == operator["totalAddedValidators"]: + return + + calldata = _encode_calldata("((uint256,uint256)[])", [[(operator_id, operator["totalAddedValidators"])]]) + motions_before = easy_track.getMotions() + + tx = easy_track.createMotion(factory, calldata, {"from": trusted_caller}) + motions = easy_track.getMotions() + + assert len(motions) == len(motions_before) + 1 + + chain.sleep(60 * 60 * 24 * 3) + chain.mine() + + easy_track.enactMotion( + motions[-1][0], + tx.events["MotionCreated"]["_evmScriptCallData"], + {"from": stranger}, + ) + + operator = simple_dvt.getNodeOperator(operator_id, False) + assert operator["totalVettedValidators"] == operator["totalAddedValidators"] + + def simple_dvt_add_node_operators(simple_dvt, stranger, input_params=[]): factory = interface.AddNodeOperators(EASYTRACK_SIMPLE_DVT_ADD_NODE_OPERATORS_FACTORY) trusted_caller = accounts.at(EASYTRACK_SIMPLE_DVT_TRUSTED_CALLER, force=True) diff --git a/utils/test/split_helpers.py b/utils/test/split_helpers.py new file mode 100644 index 00000000..9609cd5f --- /dev/null +++ b/utils/test/split_helpers.py @@ -0,0 +1,114 @@ +from utils.config import contracts +from brownie import interface, ZERO_ADDRESS + +SPLIT_DISTRIBUTOR_FEE = 0 +SPLIT_CONTROLLER = ZERO_ADDRESS + +WEI_TOLERANCE = 5 # wei tolerance to avoid rounding issue + + +def deploy_split_wallet(members, percent_allocation, deployer): + factory = contracts.split_main + + deploy_tx = factory.createSplit( + members, + percent_allocation, + SPLIT_DISTRIBUTOR_FEE, + SPLIT_CONTROLLER, + {"from": deployer or members[0]}, + ) + + deployed_instance_address = deploy_tx.events["CreateSplit"]["split"] + deployed_contract = interface.SplitWallet(deployed_instance_address) + + return (deployed_contract, deploy_tx) + + +def get_split_percentage_scale(): + return contracts.split_main.PERCENTAGE_SCALE() + + +def get_split_percent_allocation(total_members, percentage_scale): + # distribute shares evenly between participants + + shares = [percentage_scale // total_members] * total_members + remainder = percentage_scale % total_members + + for i in range(remainder): + shares[i] += 1 + + return shares + + +def get_balances_on_split_main(participants, token): + split_main = contracts.split_main + + balances = [] + for participant in participants: + balance = split_main.getERC20Balance(participant, token) + balances.append(balance) + + return balances + + +def split_and_withdraw_wsteth_rewards(split_wallet, participants, percent_allocation, percentage_scale, stranger): + split_main = contracts.split_main + wsteth = contracts.wsteth + + # check split wallet balance initial state + split_wallet_balance_before = wsteth.balanceOf(split_wallet) + wsteth_to_distribute = split_wallet_balance_before + assert wsteth_to_distribute > WEI_TOLERANCE, "no wsteth to distribute" + + # collect participants balances on split main contract before distribution + participant_balances_on_split_main_before = get_balances_on_split_main(participants, wsteth) + + # distribute rewards + distribute_tx = split_main.distributeERC20( + split_wallet, + wsteth.address, + participants, + percent_allocation, + 0, + ZERO_ADDRESS, + {"from": stranger}, + ) + distribute_event = distribute_tx.events["DistributeERC20"] + assert distribute_event["split"] == split_wallet + assert distribute_event["token"] == wsteth.address + assert wsteth_to_distribute - distribute_event["amount"] <= WEI_TOLERANCE + assert distribute_event["distributorAddress"] == ZERO_ADDRESS + + # check participants balances on split main contract after distribution + participant_balances_on_split_main_after = get_balances_on_split_main(participants, wsteth) + for index, balance_on_split_main_after in enumerate(participant_balances_on_split_main_after): + balance_on_split_main_before = participant_balances_on_split_main_before[index] + participant_income = balance_on_split_main_after - balance_on_split_main_before + + expected_participant_income = wsteth_to_distribute * percent_allocation[index] // percentage_scale + + assert participant_income > 0 + assert expected_participant_income - participant_income <= WEI_TOLERANCE + + # check that all wsteth was distributed on split main contract + total_wsteth_distributed = sum(participant_balances_on_split_main_after) + assert wsteth_to_distribute - total_wsteth_distributed <= len(participants) * WEI_TOLERANCE + + # check that a participant can withdraw wsteth from split main + participant = participants[0] + participant_balance_on_split_main = participant_balances_on_split_main_after[0] + + participant_wsteth_balance_before = wsteth.balanceOf(participant) + + withdraw_eth = 0 # withdraw only erc20 + withdraw_tx = split_main.withdraw(participant, withdraw_eth, [wsteth.address], {"from": participant}) + + participant_wsteth_balance_after = wsteth.balanceOf(participant) + withdrawn_wsteth = participant_wsteth_balance_after - participant_wsteth_balance_before + + withdraw_event = withdraw_tx.events["Withdrawal"] + assert len(withdraw_event["tokens"]) == 1 + assert withdraw_event["tokens"][0] == wsteth.address + assert withdraw_event["tokenAmounts"][0] == withdrawn_wsteth + + assert participant_balance_on_split_main - withdrawn_wsteth <= WEI_TOLERANCE diff --git a/utils/test/staking_router_helpers.py b/utils/test/staking_router_helpers.py new file mode 100644 index 00000000..cfd421f2 --- /dev/null +++ b/utils/test/staking_router_helpers.py @@ -0,0 +1,17 @@ +from utils.config import contracts +from enum import Enum + + +class ModuleStatus(Enum): + ACTIVE = 0 + PAUSED = 1 + DISABLED = 2 + + +def pause_staking_module(module_id): + staking_router, deposit_security_module = contracts.staking_router, contracts.deposit_security_module + + pause_tx = staking_router.pauseStakingModule(module_id, {"from": deposit_security_module}) + pause_event = pause_tx.events["StakingModuleStatusSet"] + assert pause_event["stakingModuleId"] == module_id + assert pause_event["status"] == ModuleStatus.PAUSED.value