diff --git a/queries/dune_v2/service_fee_status.sql b/queries/dune_v2/service_fee_status.sql new file mode 100644 index 00000000..8ce4f7ec --- /dev/null +++ b/queries/dune_v2/service_fee_status.sql @@ -0,0 +1,175 @@ +WITH +bonding_pools (pool, name, initial_funder) AS ( + SELECT from_hex(pool), name, from_hex(funder) FROM ( + VALUES {{BondingPoolData}} + ) AS _ (pool, name, funder) +), + +first_event_after_timestamp AS ( + SELECT MAX(number) FROM ethereum.blocks + WHERE time > CAST('2024-08-20 00:00:00' AS timestamp) -- CIP-48 starts bonding pool timer at midnight UTC on 20/08/24 +), + +initial_vouches AS ( + SELECT RANK() OVER ( + PARTITION BY solver, bondingPool, sender + ORDER BY evt_block_number ASC, evt_index ASC + ) AS rk, + evt_block_number, + evt_index, + solver, + cowRewardTarget, + bondingPool, + sender, + True AS active + FROM cow_protocol_ethereum.VouchRegister_evt_Vouch + WHERE evt_block_number <= (SELECT * FROM first_event_after_timestamp) + AND bondingPool IN (SELECT pool FROM bonding_pools) + AND sender IN (SELECT initial_funder FROM bonding_pools) +), + +joined_on_data AS ( + SELECT + iv.solver, + iv.cowRewardTarget AS reward_target, + iv.bondingPool AS pool, + iv.evt_block_number, + iv.evt_index, + iv.rk, + True AS active + FROM initial_vouches iv + WHERE iv.rk = 1 +), + +latest_vouches AS ( + SELECT RANK() OVER ( + PARTITION BY solver, bondingPool, sender + ORDER BY evt_block_number DESC, evt_index DESC + ) AS rk, + evt_block_number, + evt_index, + solver, + cowRewardTarget, + bondingPool, + sender, + CASE WHEN event_type = 'Vouch' THEN True ELSE False END AS active + FROM ( + SELECT + evt_block_number, + evt_index, + solver, + cowRewardTarget, + bondingPool, + sender, + 'Vouch' AS event_type + FROM cow_protocol_ethereum.VouchRegister_evt_Vouch + WHERE evt_block_number <= (SELECT * FROM first_event_after_timestamp) + AND bondingPool IN (SELECT pool FROM bonding_pools) + AND sender IN (SELECT initial_funder FROM bonding_pools) + + UNION + + SELECT + evt_block_number, + evt_index, + solver, + NULL AS cowRewardTarget, -- Invalidation does not have a reward target + bondingPool, + sender, + 'InvalidateVouch' AS event_type + FROM cow_protocol_ethereum.VouchRegister_evt_InvalidateVouch + WHERE evt_block_number <= (SELECT * FROM first_event_after_timestamp) + AND bondingPool IN (SELECT pool FROM bonding_pools) + AND sender IN (SELECT initial_funder FROM bonding_pools) + ) AS unioned_events +), + +valid_vouches AS ( + SELECT + lv.solver, + lv.cowRewardTarget AS reward_target, + lv.bondingPool AS pool + FROM latest_vouches lv + WHERE lv.rk = 1 AND lv.active = TRUE +), + +joined_on AS ( + SELECT + jd.solver, + jd.reward_target, + jd.pool, + bp.name AS pool_name, + b.time AS joined_on + FROM joined_on_data jd + JOIN ethereum.blocks b + ON b.number = jd.evt_block_number + JOIN bonding_pools bp + ON jd.pool = bp.pool +), + +named_results AS ( + SELECT + jd.solver, + CONCAT(environment, '-', s.name) AS solver_name, + jd.pool_name, + jd.pool, + jd.joined_on, + date_diff('day', date(jd.joined_on), date(NOW())) AS days_in_pool + FROM joined_on jd + JOIN cow_protocol_ethereum.solvers s + ON s.address = jd.solver + JOIN valid_vouches vv + ON vv.solver = jd.solver AND vv.pool = jd.pool +), + +ranked_named_results AS ( + SELECT + nr.solver, + nr.solver_name, + nr.pool_name, + nr.pool, + nr.joined_on, + nr.days_in_pool, + ROW_NUMBER() OVER (PARTITION BY nr.solver_name ORDER BY nr.joined_on DESC) AS rn, + COUNT(*) OVER (PARTITION BY nr.solver_name) AS solver_name_count + FROM named_results nr +), + +filtered_named_results AS ( + SELECT + rnr.solver, + rnr.solver_name, + CASE + WHEN rnr.solver_name_count > 1 THEN 'Colocation' + ELSE rnr.pool_name + END AS pool_name, + rnr.pool, + rnr.joined_on, + rnr.days_in_pool, + CASE + WHEN rnr.solver_name_count > 1 THEN DATE_ADD('month', 3, rnr.joined_on) -- Add 3 month grace period for colocated solvers + ELSE GREATEST( + DATE_ADD('month', 6, rnr.joined_on), -- Add 6 month grace period to joined_on for non colocated solvers + TIMESTAMP '2024-08-20 00:00:00' -- Introduction of CIP-48 + ) + END AS expires + FROM ranked_named_results rnr + WHERE rnr.rn = 1 +) + +SELECT + fnr.solver, + fnr.solver_name, + fnr.pool_name, + fnr.pool, + fnr.joined_on, + fnr.days_in_pool, + CASE + WHEN fnr.pool_name = 'Gnosis' THEN TIMESTAMP '2028-10-08 00:00:00' + ELSE fnr.expires + END AS expires, + CASE + WHEN NOW() > fnr.expires AND fnr.pool_name != 'Gnosis' THEN TRUE + ELSE FALSE + END AS service_fee +FROM filtered_named_results fnr; \ No newline at end of file diff --git a/src/fetch/dune.py b/src/fetch/dune.py index c152ab12..62b75c0c 100644 --- a/src/fetch/dune.py +++ b/src/fetch/dune.py @@ -123,3 +123,17 @@ def get_period_slippage(self, job_id: Optional[str] = None) -> list[DuneRecord]: ), job_id, ) + + def get_service_fee_status(self) -> list[DuneRecord]: + """ + Fetches & Returns Parsed Results for VouchRegistry query. + """ + pool_values = ",\n".join(RECOGNIZED_BONDING_POOLS) + return self._get_query_results( + query=self._parameterized_query( + query_data=QUERIES["SERVICE_FEE_STATUS"], + params=[ + QueryParameter.text_type("BondingPoolData", pool_values), + ], + ) + ) diff --git a/src/fetch/payouts.py b/src/fetch/payouts.py index 5c70a74c..46ec7a8d 100644 --- a/src/fetch/payouts.py +++ b/src/fetch/payouts.py @@ -5,8 +5,8 @@ import logging import math from dataclasses import dataclass -from datetime import timedelta - +from datetime import datetime, timedelta +from fractions import Fraction from typing import Callable import pandas @@ -27,6 +27,7 @@ CONSISTENCY_REWARD_CAP_ETH = 6 * 10**18 QUOTE_REWARD_COW = 6 * 10**18 QUOTE_REWARD_CAP_ETH = 6 * 10**14 +SERVICE_FEE_FACTOR = Fraction(15, 100) PROTOCOL_FEE_SAFE = Address("0xB64963f95215FDe6510657e719bd832BB8bb941B") @@ -46,6 +47,7 @@ "eth_slippage_wei", } REWARD_TARGET_COLUMNS = {"solver", "reward_target"} +SERVICE_FEE_COLUMNS = {"solver", "service_fee"} COMPLETE_COLUMNS = PAYMENT_COLUMNS.union(SLIPPAGE_COLUMNS).union(REWARD_TARGET_COLUMNS) NUMERICAL_COLUMNS = [ @@ -84,6 +86,7 @@ def __init__( # pylint: disable=too-many-arguments primary_reward_cow: int, secondary_reward_cow: int, quote_reward_cow: int, + service_fee: bool, ): assert secondary_reward_eth >= 0, "invalid secondary_reward_eth" assert secondary_reward_cow >= 0, "invalid secondary_reward_cow" @@ -99,6 +102,7 @@ def __init__( # pylint: disable=too-many-arguments self.secondary_reward_eth = secondary_reward_eth self.secondary_reward_cow = secondary_reward_cow self.quote_reward_cow = quote_reward_cow + self.service_fee = service_fee @classmethod def from_series(cls, frame: Series) -> RewardAndPenaltyDatum: @@ -126,19 +130,31 @@ def from_series(cls, frame: Series) -> RewardAndPenaltyDatum: secondary_reward_eth=int(frame["secondary_reward_eth"]), secondary_reward_cow=int(frame["secondary_reward_cow"]), quote_reward_cow=int(frame["quote_reward_cow"]), + service_fee=bool(frame["service_fee"]), ) def total_outgoing_eth(self) -> int: """Total outgoing amount (including slippage) for the payout.""" - return self.primary_reward_eth + self.secondary_reward_eth + self.slippage_eth + return self.total_eth_reward() + self.slippage_eth def total_cow_reward(self) -> int: """Total outgoing COW token reward""" - return self.primary_reward_cow + self.secondary_reward_cow + return int( + self.reward_scaling() + * (self.primary_reward_cow + self.secondary_reward_cow) + ) def total_eth_reward(self) -> int: """Total outgoing ETH reward""" - return self.primary_reward_eth + self.secondary_reward_eth + return int( + self.reward_scaling() + * (self.primary_reward_eth + self.secondary_reward_eth) + ) + + def reward_scaling(self) -> Fraction: + """Scaling factor for service fee + The reward is multiplied by this factor""" + return 1 - SERVICE_FEE_FACTOR * self.service_fee def is_overdraft(self) -> bool: """ @@ -151,7 +167,7 @@ def as_payouts(self) -> list[Transfer]: Isolating the logic of how solvers are paid out according to their execution costs, rewards and slippage """ - quote_reward_cow = self.quote_reward_cow + quote_reward_cow = int(self.reward_scaling() * self.quote_reward_cow) result = [] if quote_reward_cow > 0: result.append( @@ -369,7 +385,10 @@ def prepare_transfers( def validate_df_columns( - payment_df: DataFrame, slippage_df: DataFrame, reward_target_df: DataFrame + payment_df: DataFrame, + slippage_df: DataFrame, + reward_target_df: DataFrame, + service_fee_df: DataFrame, ) -> None: """ Since we are working with dataframes rather than concrete objects, @@ -386,6 +405,9 @@ def validate_df_columns( assert REWARD_TARGET_COLUMNS.issubset( set(reward_target_df.columns) ), f"Reward Target validation Failed with columns: {set(reward_target_df.columns)}" + assert SERVICE_FEE_COLUMNS.issubset( + set(service_fee_df.columns) + ), f"Service Fee validation Failed with columns: {set(service_fee_df.columns)}" def normalize_address_field(frame: DataFrame, column_name: str) -> None: @@ -394,7 +416,10 @@ def normalize_address_field(frame: DataFrame, column_name: str) -> None: def construct_payout_dataframe( - payment_df: DataFrame, slippage_df: DataFrame, reward_target_df: DataFrame + payment_df: DataFrame, + slippage_df: DataFrame, + reward_target_df: DataFrame, + service_fee_df: DataFrame, ) -> DataFrame: """ Method responsible for joining datasets related to payouts. @@ -404,17 +429,20 @@ def construct_payout_dataframe( # TODO - After CIP-20 phased in, adapt query to return `solver` like all the others slippage_df = slippage_df.rename(columns={"solver_address": "solver"}) # 1. Assert existence of required columns. - validate_df_columns(payment_df, slippage_df, reward_target_df) + validate_df_columns(payment_df, slippage_df, reward_target_df, service_fee_df) # 2. Normalize Join Column (and Ethereum Address Field) join_column = "solver" normalize_address_field(payment_df, join_column) normalize_address_field(slippage_df, join_column) normalize_address_field(reward_target_df, join_column) + normalize_address_field(service_fee_df, join_column) # 3. Merge the three dataframes (joining on solver) - merged_df = payment_df.merge(slippage_df, on=join_column, how="left").merge( - reward_target_df, on=join_column, how="left" + merged_df = ( + payment_df.merge(slippage_df, on=join_column, how="left") + .merge(reward_target_df, on=join_column, how="left") + .merge(service_fee_df, on=join_column, how="left") ) # 4. Add slippage from fees to slippage @@ -425,6 +453,44 @@ def construct_payout_dataframe( return merged_df +def construct_partner_fee_payments( + partner_fees_df: DataFrame, +) -> tuple[dict[str, int], int]: + """Compute actual partner fee payments taking partner fee tax into account + The result is a tuple. The first entry is a dictionary that contains the destination address of + a partner as a key, and the value is the amount in wei to be transferred to that address, stored + as an int. The second entry is the total amount of partner fees charged. + """ + + partner_fees_wei: dict[str, int] = {} + for _, row in partner_fees_df.iterrows(): + if row["partner_list"] is None: + continue + + # We assume the two lists used below, i.e., + # partner_list and partner_fee_eth, + # are "aligned". + + for i in range(len(row["partner_list"])): + address = row["partner_list"][i] + if address in partner_fees_wei: + partner_fees_wei[address] += int(row["partner_fee_eth"][i]) + else: + partner_fees_wei[address] = int(row["partner_fee_eth"][i]) + total_partner_fee_wei_untaxed = 0 + total_partner_fee_wei_taxed = 0 + for address, value in partner_fees_wei.items(): + total_partner_fee_wei_untaxed += value + if address == "0x63695Eee2c3141BDE314C5a6f89B98E62808d716": + partner_fees_wei[address] = int(0.90 * value) + total_partner_fee_wei_taxed += int(0.90 * value) + else: + partner_fees_wei[address] = int(0.85 * value) + total_partner_fee_wei_taxed += int(0.85 * value) + + return partner_fees_wei, total_partner_fee_wei_untaxed + + def construct_payouts( dune: DuneFetcher, orderbook: MultiInstanceDBFetcher ) -> list[Transfer]: @@ -443,6 +509,12 @@ def construct_payouts( merged_df = pandas.merge( quote_rewards_df, batch_rewards_df, on="solver", how="outer" ).fillna(0) + service_fee_df = pandas.DataFrame(dune.get_service_fee_status()) + service_fee_df["service_fee"] = [ + datetime.strptime(time_string, "%Y-%m-%d %H:%M:%S.%f %Z") <= dune.period.start + for time_string in service_fee_df["expires"] + ] + service_fee_df = service_fee_df[["solver", "service_fee"]] complete_payout_df = construct_payout_dataframe( # Fetch and extend auction data from orderbook. @@ -457,52 +529,27 @@ def construct_payouts( # Dune: Fetch Solver Slippage & Reward Targets slippage_df=pandas.DataFrame(dune.get_period_slippage()), reward_target_df=pandas.DataFrame(dune.get_vouches()), + service_fee_df=service_fee_df, ) # Sort by solver before breaking this data frame into Transfer objects. complete_payout_df = complete_payout_df.sort_values("solver") + # compute partner fees + partner_fees_wei, total_partner_fee_wei_untaxed = construct_partner_fee_payments( + partner_fees_df + ) + raw_protocol_fee_wei = int(complete_payout_df.protocol_fee_eth.sum()) + final_protocol_fee_wei = raw_protocol_fee_wei - total_partner_fee_wei_untaxed + total_partner_fee_wei_taxed = sum(partner_fees_wei.values()) + partner_fee_tax_wei = total_partner_fee_wei_untaxed - total_partner_fee_wei_taxed + 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() - raw_protocol_fee_wei = int(complete_payout_df.protocol_fee_eth.sum()) - - # We now decompose the raw_protocol_fee_wei amount into actual - # protocol fee and partner fees. For convenience, - # we use a dictionary partner_fees_wei that contains the the - # destination address of an partner as a key, and the value is the - # amount in wei to be transferred to that address, stored as an int. - - partner_fees_wei: dict[str, int] = {} - for _, row in partner_fees_df.iterrows(): - if row["partner_list"] is None: - continue - - # We assume the two lists used below, i.e., - # partner_list and partner_fee_eth, - # are "aligned". - - for i in range(len(row["partner_list"])): - address = row["partner_list"][i] - if address in partner_fees_wei: - partner_fees_wei[address] += int(row["partner_fee_eth"][i]) - else: - partner_fees_wei[address] = int(row["partner_fee_eth"][i]) - total_partner_fee_wei_untaxed = 0 - total_partner_fee_wei_taxed = 0 - partner_fee_tax_wei = 0 - for address, value in partner_fees_wei.items(): - total_partner_fee_wei_untaxed += value - if address == "0x63695Eee2c3141BDE314C5a6f89B98E62808d716": - partner_fees_wei[address] = int(0.90 * value) - total_partner_fee_wei_taxed += int(0.90 * value) - else: - partner_fees_wei[address] = int(0.85 * value) - total_partner_fee_wei_taxed += int(0.85 * value) - final_protocol_fee_wei = raw_protocol_fee_wei - total_partner_fee_wei_untaxed - partner_fee_tax_wei = total_partner_fee_wei_untaxed - total_partner_fee_wei_taxed dune.log_saver.print( + "Payment breakdown (ignoring service fees):\n" f"Performance Reward: {performance_reward / 10 ** 18:.4f}\n" f"Participation Reward: {participation_reward / 10 ** 18:.4f}\n" f"Quote Reward: {quote_reward / 10 ** 18:.4f}\n" diff --git a/src/queries.py b/src/queries.py index a8c8972c..8ac9a26c 100644 --- a/src/queries.py +++ b/src/queries.py @@ -41,6 +41,11 @@ def with_params(self, params: list[QueryParameter]) -> QueryBase: filepath="vouch_registry.sql", q_id=1541516, ), + "SERVICE_FEE_STATUS": QueryData( + name="CIP-48 Service fee status", + filepath="service_fee_status.sql", + q_id=4017925, + ), "PERIOD_SLIPPAGE": QueryData( name="Solver Slippage for Period", filepath="period_slippage.sql", diff --git a/tests/e2e/test_prices.py b/tests/e2e/test_prices.py index adb62380..b3bc8b9b 100644 --- a/tests/e2e/test_prices.py +++ b/tests/e2e/test_prices.py @@ -15,15 +15,15 @@ class TestPrices(unittest.TestCase): def setUp(self) -> None: - self.some_date = datetime.strptime("2023-02-01", "%Y-%m-%d") + self.some_date = datetime.strptime("2024-09-01", "%Y-%m-%d") self.cow_price = usd_price(TokenId.COW, self.some_date) self.eth_price = usd_price(TokenId.ETH, self.some_date) self.usdc_price = usd_price(TokenId.USDC, self.some_date) def test_usd_price(self): - self.assertEqual(self.usdc_price, 1.000519) - self.assertEqual(self.eth_price, 1590.48) - self.assertEqual(self.cow_price, 0.098138) + self.assertEqual(self.usdc_price, 1.001622) + self.assertEqual(self.eth_price, 2481.89) + self.assertEqual(self.cow_price, 0.194899) def test_token_in_usd(self): with self.assertRaises(AssertionError): @@ -65,7 +65,7 @@ def test_token_in_eth(self): def test_price_cache(self): # First call logs - day = datetime.strptime("2022-03-10", "%Y-%m-%d") # A date we used yet! + day = datetime.strptime("2024-08-01", "%Y-%m-%d") # A date we used yet! token = TokenId.USDC with self.assertLogs("src.fetch.prices", level="INFO") as cm: usd_price(token, day) diff --git a/tests/unit/test_payouts.py b/tests/unit/test_payouts.py index 5d1c68e3..2802acad 100644 --- a/tests/unit/test_payouts.py +++ b/tests/unit/test_payouts.py @@ -15,6 +15,7 @@ RewardAndPenaltyDatum, QUOTE_REWARD_COW, PROTOCOL_FEE_SAFE, + SERVICE_FEE_FACTOR, ) from src.models.accounting_period import AccountingPeriod from src.models.overdraft import Overdraft @@ -49,6 +50,7 @@ def setUp(self) -> None: ], ) ) + self.service_fee = [False, False, False, True] self.primary_reward_eth = [ 600000000000000.00000, @@ -162,25 +164,37 @@ def test_validate_df_columns(self): {"solver": [], "solver_name": [], "eth_slippage_wei": []} ) legit_reward_targets = DataFrame({"solver": [], "reward_target": []}) + legit_service_fees = DataFrame({"solver": [], "service_fee": []}) + failing_df = DataFrame({}) + with self.assertRaises(AssertionError): + validate_df_columns( + payment_df=legit_payments, + slippage_df=legit_reward_targets, + reward_target_df=legit_reward_targets, + service_fee_df=failing_df, + ) with self.assertRaises(AssertionError): validate_df_columns( payment_df=legit_payments, slippage_df=legit_reward_targets, reward_target_df=failing_df, + service_fee_df=legit_service_fees, ) with self.assertRaises(AssertionError): validate_df_columns( payment_df=legit_payments, slippage_df=failing_df, reward_target_df=legit_reward_targets, + service_fee_df=legit_service_fees, ) with self.assertRaises(AssertionError): validate_df_columns( payment_df=failing_df, slippage_df=legit_slippages, reward_target_df=legit_reward_targets, + service_fee_df=legit_service_fees, ) self.assertIsNone( @@ -188,6 +202,7 @@ def test_validate_df_columns(self): payment_df=legit_payments, slippage_df=legit_slippages, reward_target_df=legit_reward_targets, + service_fee_df=legit_service_fees, ) ) @@ -219,8 +234,16 @@ def test_construct_payouts(self): reward_targets = DataFrame( {"solver": self.solvers, "reward_target": self.reward_targets} ) + + service_fee_df = DataFrame( + {"solver": self.solvers, "service_fee": self.service_fee} + ) + result = construct_payout_dataframe( - payment_df=payments, slippage_df=slippages, reward_target_df=reward_targets + payment_df=payments, + slippage_df=slippages, + reward_target_df=reward_targets, + service_fee_df=service_fee_df, ) expected = DataFrame( { @@ -272,6 +295,12 @@ def test_construct_payouts(self): "0x0000000000000000000000000000000000000007", "0x0000000000000000000000000000000000000008", ], + "service_fee": [ + False, + False, + False, + True, + ], } ) @@ -324,6 +353,12 @@ def test_prepare_transfers(self): "0x0000000000000000000000000000000000000026", "0x5d4020b9261f01b6f8a45db929704b0ad6f5e9e6", ], + "service_fee": [ + False, + False, + False, + True, + ], } ) period = AccountingPeriod("1985-03-10", 1) @@ -356,12 +391,12 @@ def test_prepare_transfers(self): Transfer( token=Token(COW_TOKEN_ADDRESS), recipient=Address(self.reward_targets[3]), - amount_wei=180000000000000000000, + amount_wei=int(180000000000000000000 * (1 - SERVICE_FEE_FACTOR)), ), Transfer( token=Token(COW_TOKEN_ADDRESS), recipient=Address(self.reward_targets[3]), - amount_wei=54545454545454544, + amount_wei=int(54545454545454544 * (1 - SERVICE_FEE_FACTOR)), ), Transfer( token=None, @@ -400,6 +435,7 @@ def sample_record( secondary_reward: int, slippage: int, num_quotes: int, + service_fee: bool = False, ): """Assumes a conversion rate of ETH:COW <> 1:self.conversion_rate""" return RewardAndPenaltyDatum( @@ -413,6 +449,7 @@ def sample_record( secondary_reward_cow=secondary_reward * self.conversion_rate, slippage_eth=slippage, quote_reward_cow=QUOTE_REWARD_COW * num_quotes, + service_fee=service_fee, ) def test_invalid_input(self): @@ -555,6 +592,53 @@ def test_reward_datum_reward_reduces_slippage(self): ], ) + def test_performance_reward_service_fee(self): + """Sevice fee reduces COW reward.""" + primary_reward, num_quotes, service_fee = 100, 0, True + test_datum = self.sample_record( + primary_reward=primary_reward, + secondary_reward=0, + slippage=0, + num_quotes=num_quotes, + service_fee=service_fee, + ) + self.assertFalse(test_datum.is_overdraft()) + self.assertEqual( + test_datum.as_payouts(), + [ + Transfer( + token=self.cow_token, + recipient=self.reward_target, + amount_wei=int(primary_reward * (1 - SERVICE_FEE_FACTOR)) + * self.conversion_rate, + ), + ], + ) + + def test_quote_reward_service_fee(self): + """Sevice fee reduces COW reward.""" + primary_reward, num_quotes, service_fee = 0, 100, True + test_datum = self.sample_record( + primary_reward=primary_reward, + secondary_reward=0, + slippage=0, + num_quotes=num_quotes, + service_fee=service_fee, + ) + self.assertFalse(test_datum.is_overdraft()) + self.assertEqual( + test_datum.as_payouts(), + [ + Transfer( + token=self.cow_token, + recipient=self.reward_target, + amount_wei=int( + 6000000000000000000 * num_quotes * (1 - SERVICE_FEE_FACTOR) + ), + ), + ], + ) + if __name__ == "__main__": unittest.main()