Skip to content

Commit

Permalink
simple dvt rewards distribution tests
Browse files Browse the repository at this point in the history
  • Loading branch information
avsetsin committed Feb 1, 2024
1 parent 2aacab5 commit f234ba3
Show file tree
Hide file tree
Showing 11 changed files with 526 additions and 0 deletions.
7 changes: 7 additions & 0 deletions configs/config_mainnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
1 change: 1 addition & 0 deletions interfaces/0xsplits/SplitWallet.json
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"}]
1 change: 1 addition & 0 deletions interfaces/obol_split/ObolLidoSplit.json
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"}]
1 change: 1 addition & 0 deletions interfaces/obol_split/ObolLidoSplitFactory.json
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"}]
149 changes: 149 additions & 0 deletions tests/regression/test_sdvt_rewards_happy_path.py
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 tests/regression/test_sdvt_splitter_rewards_distribution.py
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,
)
8 changes: 8 additions & 0 deletions utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
58 changes: 58 additions & 0 deletions utils/test/reward_wrapper_helpers.py
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
Loading

0 comments on commit f234ba3

Please sign in to comment.