From 262e58e6a3fc33f3e0c270173556d38099193246 Mon Sep 17 00:00:00 2001 From: Felix Henneke Date: Mon, 25 Mar 2024 01:31:17 +0100 Subject: [PATCH] Implementation of CIP-38 (#345) This PR implements changes to solver rewards scripts for [CIP-38](https://snapshot.org/#/cow.eth/proposal/0xfb81daea9be89f4f1c251d53fd9d1481129b97c6f38caaddc42af7f3ce5a52ec) on ranking by surplus. The main change is that rewards are computed based on surplus (and protocol fees) and not based on surplus and network fees (and protocol fees). Additionally, there is no cost reimbursement but the slippage computation includes a contribution from fees. The implementation in this PR is quite minimal. The batch rewards query ignores network fees for payment but explicitly returns aggregated network fees to add them to slippage. The other changes are only slight renames (e.g. from `payment_eth` to `primary_reward_eth`). Test have been adapted accordingly. The slippage query does not change. Instead, when generating the final merged dataframe, network fees are explicitly added to slippage. Test have been adapted and a few comments were added. A few things can be implemented differently: - Instead of computing payments from surplus and protocol fees, one could have used `winning_score`. This is a bit simpler but would require checking somewhere that the score is consistent with the on-chain settlement. - The slippage computation could have been adapted to not exclude network fees and use external prices for converting those fees to ETH. This would require uploading explicit information on fees and their native prices to dune. This should be done at some point, but maybe not for the release. The changes to computing rewards in `batch_rewards.sql` need to be ported to dune-sync. --------- Co-authored-by: Haris Angelidakis <64154020+harisang@users.noreply.github.com> --- queries/orderbook/barn_batch_rewards.sql | 49 ++-- queries/orderbook/prod_batch_rewards.sql | 33 +-- src/fetch/payouts.py | 44 ++-- tests/queries/batch_rewards_test_db.sql | 8 +- tests/queries/test_batch_rewards.py | 26 +-- tests/unit/test_payouts.py | 274 ++++++++++++----------- 6 files changed, 218 insertions(+), 216 deletions(-) diff --git a/queries/orderbook/barn_batch_rewards.sql b/queries/orderbook/barn_batch_rewards.sql index a7407368..61be7a3a 100644 --- a/queries/orderbook/barn_batch_rewards.sql +++ b/queries/orderbook/barn_batch_rewards.sql @@ -86,7 +86,7 @@ order_protocol_fee AS ( -- impossible anyways. This query will return a division by -- zero error in that case. LEAST( - fp.surplus_max_volume_factor * os.sell_amount * os.buy_amount / (os.sell_amount - os.observed_fee), + fp.surplus_max_volume_factor / (1 - fp.surplus_max_volume_factor) * os.buy_amount, -- at most charge a fraction of volume fp.surplus_factor / (1 - fp.surplus_factor) * surplus -- charge a fraction of surplus ) @@ -96,12 +96,14 @@ order_protocol_fee AS ( fp.surplus_factor / (1 - fp.surplus_factor) * surplus -- charge a fraction of surplus ) END - WHEN fp.kind = 'volume' THEN fp.volume_factor / (1 + fp.volume_factor) * os.sell_amount + WHEN fp.kind = 'volume' THEN CASE + WHEN os.kind = 'sell' THEN + fp.volume_factor / (1 - fp.volume_factor) * os.buy_amount + WHEN os.kind = 'buy' THEN + fp.volume_factor / (1 + fp.volume_factor) * os.sell_amount + END END AS protocol_fee, - CASE - WHEN fp.kind = 'surplus' THEN os.surplus_token - WHEN fp.kind = 'volume' THEN os.sell_token - END AS protocol_fee_token + os.surplus_token AS protocol_fee_token FROM order_surplus os JOIN fee_policies fp -- contains protocol fee policy @@ -160,17 +162,17 @@ reward_data AS ( case when block_number is not null and block_number > block_deadline then 0 - else coalesce(execution_cost, 0) + else coalesce(execution_cost, 0) -- if block_number is null, execution cost is 0 end as execution_cost, case when block_number is not null and block_number > block_deadline then 0 - else coalesce(surplus, 0) + else coalesce(surplus, 0) -- if block_number is null, surplus is 0 end as surplus, case when block_number is not null and block_number > block_deadline then 0 - else coalesce(fee, 0) + else coalesce(fee, 0) -- if block_number is null, fee is 0 end as fee, -- scores winning_score, @@ -201,14 +203,14 @@ reward_per_auction as ( surplus, protocol_fee, -- the protocol fee fee - network_fee_correction as network_fee, -- the network fee - surplus + protocol_fee + fee - network_fee_correction - reference_score as uncapped_payment_eth, + surplus + protocol_fee - reference_score as uncapped_payment, -- Capped Reward = CLAMP_[-E, E + exec_cost](uncapped_reward_eth) LEAST( GREATEST( - {{EPSILON_LOWER}}, - surplus + protocol_fee + fee - network_fee_correction - reference_score + surplus + protocol_fee - reference_score ), - {{EPSILON_UPPER}} + execution_cost + {{EPSILON_UPPER}} ) as capped_payment, winning_score, reference_score, @@ -235,17 +237,9 @@ participation_counts as ( primary_rewards as ( SELECT rpt.solver, - SUM(capped_payment) as payment_wei, - SUM(execution_cost) as exececution_cost_wei - FROM - reward_per_auction rpt - GROUP BY - solver -), -protocol_fees as ( - SELECT - solver, - SUM(protocol_fee) as protocol_fee_wei + SUM(capped_payment) as payment, + SUM(protocol_fee) as protocol_fee, + SUM(network_fee) as network_fee FROM reward_per_auction rpt GROUP BY @@ -254,18 +248,17 @@ protocol_fees as ( aggregate_results as ( SELECT concat('0x', encode(pc.solver, 'hex')) as solver, - coalesce(payment_wei, 0) as payment_eth, - coalesce(exececution_cost_wei, 0) as execution_cost_eth, + coalesce(payment, 0) as primary_reward_eth, num_participating_batches, - coalesce(protocol_fee_wei, 0) as protocol_fee_eth + coalesce(protocol_fee, 0) as protocol_fee_eth, + coalesce(network_fee, 0) as network_fee_eth FROM participation_counts pc LEFT OUTER JOIN primary_rewards pr ON pr.solver = pc.solver - LEFT OUTER JOIN protocol_fees pf ON pf.solver = pc.solver ) -- select * from aggregate_results order by - solver \ No newline at end of file + solver diff --git a/queries/orderbook/prod_batch_rewards.sql b/queries/orderbook/prod_batch_rewards.sql index 621f701d..61be7a3a 100644 --- a/queries/orderbook/prod_batch_rewards.sql +++ b/queries/orderbook/prod_batch_rewards.sql @@ -162,17 +162,17 @@ reward_data AS ( case when block_number is not null and block_number > block_deadline then 0 - else coalesce(execution_cost, 0) + else coalesce(execution_cost, 0) -- if block_number is null, execution cost is 0 end as execution_cost, case when block_number is not null and block_number > block_deadline then 0 - else coalesce(surplus, 0) + else coalesce(surplus, 0) -- if block_number is null, surplus is 0 end as surplus, case when block_number is not null and block_number > block_deadline then 0 - else coalesce(fee, 0) + else coalesce(fee, 0) -- if block_number is null, fee is 0 end as fee, -- scores winning_score, @@ -203,14 +203,14 @@ reward_per_auction as ( surplus, protocol_fee, -- the protocol fee fee - network_fee_correction as network_fee, -- the network fee - surplus + protocol_fee + fee - network_fee_correction - reference_score as uncapped_payment_eth, + surplus + protocol_fee - reference_score as uncapped_payment, -- Capped Reward = CLAMP_[-E, E + exec_cost](uncapped_reward_eth) LEAST( GREATEST( - {{EPSILON_LOWER}}, - surplus + protocol_fee + fee - network_fee_correction - reference_score + surplus + protocol_fee - reference_score ), - {{EPSILON_UPPER}} + execution_cost + {{EPSILON_UPPER}} ) as capped_payment, winning_score, reference_score, @@ -237,17 +237,9 @@ participation_counts as ( primary_rewards as ( SELECT rpt.solver, - SUM(capped_payment) as payment_wei, - SUM(execution_cost) as exececution_cost_wei - FROM - reward_per_auction rpt - GROUP BY - solver -), -protocol_fees as ( - SELECT - solver, - SUM(protocol_fee) as protocol_fee_wei + SUM(capped_payment) as payment, + SUM(protocol_fee) as protocol_fee, + SUM(network_fee) as network_fee FROM reward_per_auction rpt GROUP BY @@ -256,14 +248,13 @@ protocol_fees as ( aggregate_results as ( SELECT concat('0x', encode(pc.solver, 'hex')) as solver, - coalesce(payment_wei, 0) as payment_eth, - coalesce(exececution_cost_wei, 0) as execution_cost_eth, + coalesce(payment, 0) as primary_reward_eth, num_participating_batches, - coalesce(protocol_fee_wei, 0) as protocol_fee_eth + coalesce(protocol_fee, 0) as protocol_fee_eth, + coalesce(network_fee, 0) as network_fee_eth FROM participation_counts pc LEFT OUTER JOIN primary_rewards pr ON pr.solver = pc.solver - LEFT OUTER JOIN protocol_fees pf ON pf.solver = pc.solver ) -- select * diff --git a/src/fetch/payouts.py b/src/fetch/payouts.py index 0921f2a9..ff24de3d 100644 --- a/src/fetch/payouts.py +++ b/src/fetch/payouts.py @@ -32,13 +32,13 @@ PAYMENT_COLUMNS = { "solver", - "payment_eth", - "execution_cost_eth", + "primary_reward_eth", + "primary_reward_cow", "secondary_reward_eth", - "reward_cow", "secondary_reward_cow", "quote_reward_cow", "protocol_fee_eth", + "network_fee_eth", } SLIPPAGE_COLUMNS = { "solver", @@ -49,10 +49,8 @@ COMPLETE_COLUMNS = PAYMENT_COLUMNS.union(SLIPPAGE_COLUMNS).union(REWARD_TARGET_COLUMNS) NUMERICAL_COLUMNS = [ - "payment_eth", - "execution_cost_eth", - "reward_eth", - "reward_cow", + "primary_reward_eth", + "primary_reward_cow", "secondary_reward_cow", "secondary_reward_eth", "quote_reward_cow", @@ -79,24 +77,22 @@ def __init__( # pylint: disable=too-many-arguments solver: Address, solver_name: str, reward_target: Address, - exec_cost: int, - payment_eth: int, + primary_reward_eth: int, secondary_reward_eth: int, slippage_eth: int, primary_reward_cow: int, secondary_reward_cow: int, quote_reward_cow: int, ): - assert exec_cost >= 0, f"invalid execution cost {exec_cost}" assert secondary_reward_eth >= 0, "invalid secondary_reward_eth" assert secondary_reward_cow >= 0, "invalid secondary_reward_cow" + assert quote_reward_cow >= 0, "invalid quote_reward_cow" self.solver = solver self.solver_name = solver_name self.reward_target = reward_target - self.exec_cost = exec_cost - self.payment_eth = payment_eth self.slippage_eth = slippage_eth + self.primary_reward_eth = primary_reward_eth self.primary_reward_cow = primary_reward_cow self.secondary_reward_eth = secondary_reward_eth self.secondary_reward_cow = secondary_reward_cow @@ -120,10 +116,9 @@ def from_series(cls, frame: Series) -> RewardAndPenaltyDatum: solver=Address(solver), solver_name=frame["solver_name"], reward_target=Address(reward_target), - payment_eth=int(frame["payment_eth"]), slippage_eth=slippage, - primary_reward_cow=int(frame["reward_cow"]), - exec_cost=int(frame["execution_cost_eth"]), + primary_reward_eth=int(frame["primary_reward_eth"]), + primary_reward_cow=int(frame["primary_reward_cow"]), secondary_reward_eth=int(frame["secondary_reward_eth"]), secondary_reward_cow=int(frame["secondary_reward_cow"]), quote_reward_cow=int(frame["quote_reward_cow"]), @@ -131,7 +126,7 @@ def from_series(cls, frame: Series) -> RewardAndPenaltyDatum: def total_outgoing_eth(self) -> int: """Total outgoing amount (including slippage) for the payout.""" - return self.payment_eth + self.secondary_reward_eth + self.slippage_eth + return self.primary_reward_eth + self.secondary_reward_eth + self.slippage_eth def total_cow_reward(self) -> int: """Total outgoing COW token reward""" @@ -139,7 +134,7 @@ def total_cow_reward(self) -> int: def total_eth_reward(self) -> int: """Total outgoing ETH reward""" - return self.payment_eth - self.exec_cost + self.secondary_reward_eth + return self.primary_reward_eth + self.secondary_reward_eth def is_overdraft(self) -> bool: """ @@ -168,7 +163,7 @@ def as_payouts(self) -> list[Transfer]: total_eth_reward = int(self.total_eth_reward()) total_cow_reward = int(self.total_cow_reward()) - reimbursement_eth = int(self.exec_cost + self.slippage_eth) + reimbursement_eth = int(self.slippage_eth) # We do not have access to token conversion here, but we do have other converted values # x_eth:x_cow = y_eth:y_cow --> y_cow = y_eth * x_cow / x_eth reimbursement_cow = ( @@ -270,12 +265,11 @@ def extend_payment_df(pdf: DataFrame, converter: TokenConversion) -> DataFrame: This is evaluated in both ETH and COW (for different use cases). """ # Note that this can be negative! - pdf["reward_eth"] = pdf["payment_eth"] - pdf["execution_cost_eth"] - pdf["reward_cow"] = pdf["reward_eth"].apply(converter.eth_to_token) + pdf["primary_reward_cow"] = pdf["primary_reward_eth"].apply(converter.eth_to_token) secondary_allocation = max( min( - PERIOD_BUDGET_COW - pdf["reward_cow"].sum(), + PERIOD_BUDGET_COW - pdf["primary_reward_cow"].sum(), converter.eth_to_token(CONSISTENCY_REWARD_CAP_ETH), ), 0, @@ -383,6 +377,12 @@ def construct_payout_dataframe( merged_df = payment_df.merge(slippage_df, on=join_column, how="left").merge( reward_target_df, on=join_column, how="left" ) + + # 4. Add slippage from fees to slippage + merged_df["eth_slippage_wei"] = ( + merged_df["eth_slippage_wei"].fillna(0) + merged_df["network_fee_eth"] + ) + return merged_df @@ -417,7 +417,7 @@ def construct_payouts( # Sort by solver before breaking this data frame into Transfer objects. complete_payout_df = complete_payout_df.sort_values("solver") - performance_reward = complete_payout_df["reward_cow"].sum() + performance_reward = complete_payout_df["primary_reward_cow"].sum() participation_reward = complete_payout_df["secondary_reward_cow"].sum() quote_reward = complete_payout_df["quote_reward_cow"].sum() protocol_fee = complete_payout_df["protocol_fee_eth"].sum() diff --git a/tests/queries/batch_rewards_test_db.sql b/tests/queries/batch_rewards_test_db.sql index c61c745f..20f0937b 100644 --- a/tests/queries/batch_rewards_test_db.sql +++ b/tests/queries/batch_rewards_test_db.sql @@ -179,12 +179,12 @@ VALUES (1, '\x5222222222222222222222222222222222222222'::bytea), (56, '\x02'::bytea); INSERT INTO settlement_scores (auction_id, winning_score, reference_score, winner, block_deadline, simulation_block) -VALUES (1, 5000000000000000000, 4000000000000000000, '\x5111111111111111111111111111111111111111'::bytea, 10, 0), - (2, 6000000000000000000, 3000000000000000000, '\x5222222222222222222222222222222222222222'::bytea, 11, 1), +VALUES (1, 6000000000000000000, 4000000000000000000, '\x5111111111111111111111111111111111111111'::bytea, 10, 0), + (2, 12000000000000000000, 3000000000000000000, '\x5222222222222222222222222222222222222222'::bytea, 11, 1), (3, 21000000000000000000, 3000000000000000000, '\x5111111111111111111111111111111111111111'::bytea, 12, 2), (5, 5000000000000000000, 3000000000000000000, '\x5111111111111111111111111111111111111111'::bytea, 14, 4), -- jump in auction id (6, 10000000000000000000, 9000000000000000000, '\x5111111111111111111111111111111111111111'::bytea, 15, 5), -- settled too late - (7, 5000000000000000000, 0, '\x5111111111111111111111111111111111111111'::bytea, 30, 6), -- no competition + (7, 6000000000000000000, 0, '\x5111111111111111111111111111111111111111'::bytea, 30, 6), -- no competition (8, 5000000000000000000, 0, '\x5111111111111111111111111111111111111111'::bytea, 35, 7), -- no competition, failed transaction (9, 5000000000000000000, 1000000000000000000, '\x5111111111111111111111111111111111111111'::bytea, 36, 8), -- score larger than quality (10, 5000000000000000000, 4000000000000000000, '\x5333333333333333333333333333333333333333'::bytea, 37, 9), -- participant with net negative payment @@ -235,7 +235,7 @@ VALUES ('\x01'::bytea, '\x01'::bytea, '\x02'::bytea, 95000000, 94000000000000000 INSERT INTO trades (block_number, log_index, order_uid, sell_amount, buy_amount, fee_amount) VALUES (51, 0, '\x01'::bytea, 100000000, 95000000000000000000, 5000000), -(52, 0, '\x02'::bytea, 106000000, 100000000000000000000, 5000000), +(52, 0, '\x02'::bytea, 105000000, 100000000000000000000, 5000000), (53, 0, '\x03'::bytea, 100000000, 101000000000000000000, 0), (54, 0, '\x04'::bytea, 99000000, 100000000000000000000, 0), (55, 0, '\x05'::bytea, 100000000, 95000000000000000000, 0), diff --git a/tests/queries/test_batch_rewards.py b/tests/queries/test_batch_rewards.py index a55f01c3..5d6edfb8 100644 --- a/tests/queries/test_batch_rewards.py +++ b/tests/queries/test_batch_rewards.py @@ -28,22 +28,14 @@ def test_get_batch_rewards(self): "0x5333333333333333333333333333333333333333", "0x5444444444444444444444444444444444444444", ], - "payment_eth": [ - 9535064849462312.0, - 10.5e15, - 6600000000000000.00000, - 12450000000000000.00000, + "primary_reward_eth": [ + 2071357035553330.0, # 3 * 1e18 * 5e14 / 1e18 (surplus) + 571357035553330 (protocol fee) + 3519801980198020.0, + 6000000000000000.00000, + 12000000000000000.00000, -10000000000000000.00000, 0.00000, ], - "execution_cost_eth": [ - 7500000000000000.0, - 7500000000000000.0, - 800000000000000.00000, - 450000000000000.00000, - 0.00000, - 0.00000, - ], "num_participating_batches": [ 3, 3, @@ -60,6 +52,14 @@ def test_get_batch_rewards(self): 0.0, 0.0, ], + "network_fee_eth": [ + 7463707813908982.0, # almost 2500000000000000 + 3000000000000000 + 2500000000000000 - 5.748876684972541e14 + 6980198019801980.0, # 2500000000000000 + 4000000000000000 + 2500000000000000 - 2.0198019801980198e15 + 1050000000000000.0, + 400000000000000.0, + 0.0, + 0.0, + ], } ) self.assertIsNone(pandas.testing.assert_frame_equal(expected, batch_rewards)) diff --git a/tests/unit/test_payouts.py b/tests/unit/test_payouts.py index 3651685a..4673f910 100644 --- a/tests/unit/test_payouts.py +++ b/tests/unit/test_payouts.py @@ -50,18 +50,12 @@ def setUp(self) -> None: ) ) - self.eth_payments = [ + self.primary_reward_eth = [ 600000000000000.00000, - 10450000000000000.00000, + 12000000000000000.00000, -10000000000000000.00000, 0.00000, ] - self.execution_costs = [ - 800000000000000.00000, - 450000000000000.00000, - 0.00000, - 0.00000, - ] self.batch_participation = [ 7, 2, @@ -74,40 +68,45 @@ def setUp(self) -> None: 0.0, 0.0, ] + self.network_fee_eth = [ + 2000000000000000.0, + 4000000000000000.0, + 0.0, + 0.0, + ] # Mocking TokenConversion! self.mock_converter = TokenConversion( eth_to_token=lambda t: int(t * 1000), token_to_eth=lambda t: t // 1000 ) def test_extend_payment_df(self): - base_data_dict: dict = { + base_data_dict = { "solver": self.solvers, "num_quotes": self.num_quotes, - "payment_eth": self.eth_payments, - "execution_cost_eth": self.execution_costs, + "primary_reward_eth": self.primary_reward_eth, "num_participating_batches": self.batch_participation, "protocol_fee_eth": self.protocol_fee_eth, + "network_fee_eth": self.network_fee_eth, } base_payout_df = DataFrame(base_data_dict) result = extend_payment_df(base_payout_df, converter=self.mock_converter) expected_data_dict = { "solver": self.solvers, "num_quotes": self.num_quotes, - "payment_eth": self.eth_payments, - "execution_cost_eth": self.execution_costs, - "num_participating_batches": self.batch_participation, - "protocol_fee_eth": self.protocol_fee_eth, - "reward_eth": [ - -200000000000000.00000, - 10000000000000000.00000, + "primary_reward_eth": [ + 600000000000000.00000, + 12000000000000000.00000, -10000000000000000.00000, 0.00000, ], - "reward_cow": [ - -200000000000000000, - 10000000000000000000, - -10000000000000000000, - 0, + "num_participating_batches": self.batch_participation, + "protocol_fee_eth": self.protocol_fee_eth, + "network_fee_eth": self.network_fee_eth, + "primary_reward_cow": [ + 600000000000000000.0, + 12000000000000000000.0, + -10000000000000000000.0, + 0.0, ], "secondary_reward_cow": [ 1909090909090909000000.00000, @@ -149,11 +148,11 @@ def test_validate_df_columns(self): { "solver": [], "payment_eth": [], - "execution_cost_eth": [], "num_participating_batches": [], "protocol_fee_eth": [], - "reward_eth": [], - "reward_cow": [], + "network_fee_eth": [], + "primary_reward_eth": [], + "primary_reward_cow": [], "secondary_reward_cow": [], "secondary_reward_eth": [], "quote_reward_cow": [], @@ -198,10 +197,10 @@ def test_construct_payouts(self): { "solver": self.solvers, "num_quotes": self.num_quotes, - "payment_eth": self.eth_payments, - "execution_cost_eth": self.execution_costs, + "primary_reward_eth": self.primary_reward_eth, "num_participating_batches": self.batch_participation, "protocol_fee_eth": self.protocol_fee_eth, + "network_fee_eth": self.network_fee_eth, } ), converter=self.mock_converter, @@ -227,8 +226,7 @@ def test_construct_payouts(self): { "solver": self.solvers, "num_quotes": self.num_quotes, - "payment_eth": [600000000000000.0, 1.045e16, -1e16, 0.0], - "execution_cost_eth": [800000000000000.0, 450000000000000.0, 0.0, 0.0], + "primary_reward_eth": [600000000000000.0, 1.2e16, -1e16, 0.0], "num_participating_batches": [7, 2, 7, 6], "protocol_fee_eth": [ 1000000000000000.0, @@ -236,12 +234,17 @@ def test_construct_payouts(self): 0.0, 0.0, ], - "reward_eth": [-200000000000000.0, 1e16, -1e16, 0.0], - "reward_cow": [ - -200000000000000000, - 10000000000000000000, - -10000000000000000000, - 0, + "network_fee_eth": [ + 2000000000000000.0, + 4000000000000000.0, + 0.0, + 0.0, + ], + "primary_reward_cow": [ + 600000000000000000.0, + 12000000000000000000.0, + -10000000000000000000.0, + 0.0, ], "secondary_reward_cow": [ 1909090909090909000000.00000, @@ -262,7 +265,7 @@ def test_construct_payouts(self): 12000000000000000000.00000, ], "solver_name": ["S_1", "S_2", "S_3", None], - "eth_slippage_wei": [1.0, 0.0, -1.0, None], + "eth_slippage_wei": [2000000000000001.0, 4000000000000000.0, -1.0, 0.0], "reward_target": [ "0x0000000000000000000000000000000000000005", "0x0000000000000000000000000000000000000006", @@ -272,7 +275,7 @@ def test_construct_payouts(self): } ) - self.assertIsNone(pandas.testing.assert_frame_equal(result, expected)) + self.assertIsNone(pandas.testing.assert_frame_equal(expected, result)) def test_prepare_transfers(self): # Need Example of every possible scenario @@ -280,15 +283,14 @@ def test_prepare_transfers(self): { "solver": self.solvers, "num_quotes": self.num_quotes, - "payment_eth": [600000000000000.0, 1.045e16, -1e16, 0.0], - "execution_cost_eth": [800000000000000.0, 450000000000000.0, 0.0, 0.0], + "primary_reward_eth": [600000000000000.0, 1.2e16, -1e16, 0.0], "protocol_fee_eth": self.protocol_fee_eth, - "reward_eth": [-200000000000000.0, 1e16, -1e16, 0.0], - "reward_cow": [ - -200000000000000000, - 10000000000000000000, - -10000000000000000000, - 0, + "network_fee_eth": [100.0, 200.0, 300.0, 0.0], + "primary_reward_cow": [ + 600000000000000000.0, + 12000000000000000000.0, + -10000000000000000000.0, + 0.0, ], "secondary_reward_cow": [ 6.363636363636364e16, @@ -321,22 +323,21 @@ def test_prepare_transfers(self): period = AccountingPeriod("1985-03-10", 1) payout_transfers = prepare_transfers(full_payout_data, period) self.assertEqual( - payout_transfers.transfers, [ Transfer( token=None, recipient=Address(self.solvers[0]), - amount_wei=663636363636364, + amount_wei=1, ), Transfer( - token=None, - recipient=Address(self.solvers[1]), - amount_wei=450000000000000, + token=Token(COW_TOKEN_ADDRESS), + recipient=Address(self.reward_targets[0]), + amount_wei=663636363636363640, ), Transfer( token=Token(COW_TOKEN_ADDRESS), recipient=Address(self.reward_targets[1]), - amount_wei=10018181818181818180, + amount_wei=12018181818181818180, ), Transfer( token=Token(COW_TOKEN_ADDRESS), @@ -359,6 +360,7 @@ def test_prepare_transfers(self): amount_wei=3000000000000000, ), ], + payout_transfers.transfers, ) self.assertEqual( @@ -384,9 +386,8 @@ def setUp(self) -> None: def sample_record( self, - payment: int, - cost: int, - participation: int, + primary_reward: int, + secondary_reward: int, slippage: int, num_quotes: int, ): @@ -395,126 +396,143 @@ def sample_record( solver=self.solver, solver_name=self.solver_name, reward_target=self.reward_target, - payment_eth=payment, - exec_cost=cost, - primary_reward_cow=(payment - cost) * self.conversion_rate, - secondary_reward_eth=participation, - secondary_reward_cow=participation * self.conversion_rate, + primary_reward_eth=primary_reward, + primary_reward_cow=primary_reward * self.conversion_rate, + secondary_reward_eth=secondary_reward, + secondary_reward_cow=secondary_reward * self.conversion_rate, slippage_eth=slippage, quote_reward_cow=QUOTE_REWARD_COW * num_quotes, ) def test_invalid_input(self): + """Test that negative and secondary and quote rewards throw an error.""" + + # invalid secondary reward with self.assertRaises(AssertionError): - self.sample_record(0, -1, 0, 0, 0) + self.sample_record(0, -1, 0, 0) + # invalid quote reward with self.assertRaises(AssertionError): - self.sample_record(0, 0, -1, 0, 0) + self.sample_record(0, 0, 0, -1) def test_reward_datum_0_0_0_0(self): - test_datum = self.sample_record(0, 0, 0, 0, 0) + """Without data there is no payout and no overdraft.""" + test_datum = self.sample_record(0, 0, 0, 0) self.assertFalse(test_datum.is_overdraft()) self.assertEqual(test_datum.as_payouts(), []) - def test_reward_datum_1_1_0_0(self): - cost = 1 - test_datum = self.sample_record(1, cost, 0, 0, 0) - self.assertFalse(test_datum.is_overdraft()) - self.assertEqual( - test_datum.as_payouts(), - [Transfer(token=None, recipient=self.solver, amount_wei=cost)], - ) + def test_reward_datum_pm1_0_0_0(self): + """Primary reward only.""" - def test_reward_datum_3_2_0_minus1(self): - payment, cost, participation, slippage = 3, 2, 0, -1 - test_datum = self.sample_record(payment, cost, participation, slippage, 0) + # positive reward is paid in COW + primary_reward = 1 + test_datum = self.sample_record(primary_reward, 0, 0, 0) self.assertFalse(test_datum.is_overdraft()) self.assertEqual( test_datum.as_payouts(), [ - Transfer( - token=None, - recipient=self.solver, - amount_wei=cost + slippage, - ), Transfer( token=self.cow_token, recipient=self.reward_target, - amount_wei=(payment - cost) * self.conversion_rate, - ), + amount_wei=primary_reward * self.conversion_rate, + ) ], ) - def test_reward_datum_cost_exceeds_payment_degenerate(self): - # Degenerate Case! - payment, cost, participation, slippage = 1, 10, 0, -1 - test_datum = self.sample_record(payment, cost, participation, slippage, 0) + # negative reward gives overdraft + primary_reward = -1 + test_datum = self.sample_record(primary_reward, 0, 0, 0) + self.assertTrue(test_datum.is_overdraft()) + self.assertEqual(test_datum.as_payouts(), []) + + def test_reward_datum_0_0_pm1_0(self): + """Slippag only.""" + + # positive slippage is paid in ETH + slippage = 1 + test_datum = self.sample_record(0, 0, slippage, 0) self.assertFalse(test_datum.is_overdraft()) self.assertEqual( test_datum.as_payouts(), - [], + [Transfer(token=None, recipient=self.solver, amount_wei=slippage)], ) - def test_reward_datum_cost_exceeds_payment_non_degenerate(self): - # Payment + Slippage combined do not exceed costs so only that is returned - - triplets = [(1, 0, 1), (2, 0, -1), (1, 1, 1)] - cost = max(sum(x) for x in triplets) + 1 - - for payment, participation, slippage in triplets: - test_datum = self.sample_record(payment, cost, participation, slippage, 0) - self.assertFalse(test_datum.is_overdraft()) - self.assertLess(test_datum.total_outgoing_eth(), cost) - self.assertEqual( - test_datum.as_payouts(), - [ - Transfer( - token=None, - recipient=self.solver, - amount_wei=test_datum.total_outgoing_eth(), - ) - ], - ) - - def test_reward_datum_overdraft(self): - # Any time when payment + participation + slippage < 0 - triplets = [ - (-1, 0, 0), - (0, 0, -1), - ] - for payment, participation, slippage in triplets: - for cost in [0, 1, 100]: - # Doesn't matter their costs, they are in overdraft state! - rec = self.sample_record(payment, cost, participation, slippage, 0) - self.assertTrue(rec.is_overdraft()) - - def test_reward_datum_1_1_1_1(self): - payment, cost, participation, slippage = 1, 1, 1, 1 - test_datum = self.sample_record(payment, cost, participation, slippage, 0) + # negative slippage gives overdraft + slippage = -1 + test_datum = self.sample_record(0, 0, slippage, 0) + self.assertTrue(test_datum.is_overdraft()) + self.assertEqual(test_datum.as_payouts(), []) + def test_reward_datum_0_0_0_1(self): + """Quote rewards only.""" + num_quotes = 1 + test_datum = self.sample_record(0, 0, 0, num_quotes) self.assertFalse(test_datum.is_overdraft()) self.assertEqual( - test_datum.total_cow_reward(), participation * self.conversion_rate + test_datum.as_payouts(), + [ + Transfer( + token=self.cow_token, + recipient=self.reward_target, + amount_wei=6000000000000000000 * num_quotes, + ) + ], ) + + def test_reward_datum_4_2_1_0(self): + """COW payment for rewards, ETH payment for slippage.""" + primary_reward, secondary_reward, slippage, num_quotes = 4, 2, 1, 0 + test_datum = self.sample_record(primary_reward, secondary_reward, slippage, 0) + self.assertFalse(test_datum.is_overdraft()) self.assertEqual( test_datum.as_payouts(), [ Transfer( token=None, recipient=self.solver, - amount_wei=cost + slippage, + amount_wei=slippage, ), Transfer( token=self.cow_token, recipient=self.reward_target, - amount_wei=test_datum.total_cow_reward(), + amount_wei=(primary_reward + secondary_reward) + * self.conversion_rate, ), ], ) - def test_payout_negative_payments(self): - payment, cost, participation, slippage = -1, 1, 1, 1 - test_datum = self.sample_record(payment, cost, participation, slippage, 0) + def test_reward_datum_slippage_reduces_reward(self): + """Negative slippage reduces COW reward.""" + primary_reward, secondary_reward, slippage, num_quotes = 4, 2, -1, 0 + test_datum = self.sample_record(primary_reward, secondary_reward, slippage, 0) + self.assertFalse(test_datum.is_overdraft()) + self.assertEqual( + test_datum.as_payouts(), + [ + Transfer( + token=self.cow_token, + recipient=self.reward_target, + amount_wei=(primary_reward + secondary_reward + slippage) + * self.conversion_rate, + ), + ], + ) + + def test_reward_datum_slippage_exceeds_reward(self): + """Negative slippage leads to overtraft.""" + primary_reward, participation, slippage = 1, 2, -4 + test_datum = self.sample_record(primary_reward, participation, slippage, 0) + self.assertTrue(test_datum.is_overdraft()) + self.assertEqual(test_datum.as_payouts(), []) + + def test_reward_datum_reward_reduces_slippage(self): + """Negative reward reduces ETH slippage payment.""" + primary_reward, secondary_reward, slippage = -2, 1, 3 + test_datum = self.sample_record(primary_reward, secondary_reward, slippage, 0) + self.assertEqual( + test_datum.total_outgoing_eth(), + primary_reward + secondary_reward + slippage, + ) self.assertEqual( test_datum.as_payouts(), [