-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
simple dvt rewards distribution tests
- Loading branch information
Showing
11 changed files
with
526 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"}] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"}] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"}] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
) |
138 changes: 138 additions & 0 deletions
138
tests/regression/test_sdvt_splitter_rewards_distribution.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.