From c34757e5eeb46570183b3234ea5d0250e2d40c64 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 10 Jul 2023 21:05:12 +0200 Subject: [PATCH] [Tech Debt] Remove Old Dead Code (pre CIP-20) (#311) --- queries/dune_v2/eth_spent.sql | 12 - queries/dune_v2/risk_free_batches.sql | 51 ---- queries/dune_v2/trade_counts.sql | 8 - queries/orderbook/order_rewards.sql | 31 -- src/constants.py | 7 + src/fetch/cow_rewards.py | 47 --- src/fetch/dune.py | 86 +----- src/fetch/payouts.py | 2 +- src/fetch/transfer_file.py | 21 +- src/models/overdraft.py | 19 -- src/models/period_totals.py | 16 - src/models/slippage.py | 69 ----- src/models/split_transfers.py | 191 ------------ src/models/transfer.py | 53 ---- src/models/vouch.py | 38 --- src/pg_client.py | 90 ------ src/queries.py | 15 - src/utils/script_args.py | 10 +- tests/e2e/test_get_transfers.py | 38 --- tests/e2e/test_per_batch_rewards.py | 76 ----- tests/e2e/test_transfer_file.py | 79 ----- tests/unit/test_models.py | 90 ------ tests/unit/test_reward_aggregation.py | 90 ------ tests/unit/test_split_transfer.py | 409 -------------------------- 24 files changed, 20 insertions(+), 1528 deletions(-) delete mode 100644 queries/dune_v2/eth_spent.sql delete mode 100644 queries/dune_v2/risk_free_batches.sql delete mode 100644 queries/dune_v2/trade_counts.sql delete mode 100644 queries/orderbook/order_rewards.sql delete mode 100644 src/fetch/cow_rewards.py delete mode 100644 src/models/period_totals.py delete mode 100644 src/models/slippage.py delete mode 100644 src/models/split_transfers.py delete mode 100644 src/models/vouch.py delete mode 100644 tests/e2e/test_get_transfers.py delete mode 100644 tests/e2e/test_per_batch_rewards.py delete mode 100644 tests/e2e/test_transfer_file.py delete mode 100644 tests/unit/test_reward_aggregation.py delete mode 100644 tests/unit/test_split_transfer.py diff --git a/queries/dune_v2/eth_spent.sql b/queries/dune_v2/eth_spent.sql deleted file mode 100644 index 2629a642..00000000 --- a/queries/dune_v2/eth_spent.sql +++ /dev/null @@ -1,12 +0,0 @@ --- V3: https://dune.com/queries/1320174 -select - solver_address as receiver, - sum(gas_price * gas_used) as amount, - count(*) as num_transactions -from cow_protocol_ethereum.batches - join cow_protocol_ethereum.solvers - on solver_address = address -where block_time between cast('{{StartTime}}' as timestamp) and cast('{{EndTime}}' as timestamp) - and environment not in ('services', 'test') -group by solver_address -order by receiver \ No newline at end of file diff --git a/queries/dune_v2/risk_free_batches.sql b/queries/dune_v2/risk_free_batches.sql deleted file mode 100644 index ed9f562e..00000000 --- a/queries/dune_v2/risk_free_batches.sql +++ /dev/null @@ -1,51 +0,0 @@ -with -interactions as ( - select - selector, - target, - case - when selector in ( - '0x095ea7b3', -- approve - '0x2e1a7d4d', -- withdraw - '0xa9059cbb', -- transfer - '0x23b872dd' -- transferFrom - ) then true - else false - end as risk_free - from gnosis_protocol_v2_ethereum.GPv2Settlement_evt_Interaction - where evt_block_time between '{{StartTime}}' and '{{EndTime}}' -), - -no_interactions as ( - select tx_hash - from cow_protocol_ethereum.batches - where block_time between '{{StartTime}}' and '{{EndTime}}' - and tx_hash not in ( - select evt_tx_hash - from gnosis_protocol_v2_ethereum.GPv2Settlement_evt_Interaction - where evt_block_time between '{{StartTime}}' and '{{EndTime}}' - ) -), - -batch_interaction_counts as ( - select - tx_hash, - count(*) as num_interactions, - sum(case when risk_free = true then 1 else 0 end) as risk_free - from cow_protocol_ethereum.batches b - inner join gnosis_protocol_v2_ethereum.GPv2Settlement_evt_Interaction i - on tx_hash = i.evt_tx_hash - inner join interactions i2 - on i.selector = i2.selector - and i.target = i2.target - where b.block_time between '{{StartTime}}' and '{{EndTime}}' - group by tx_hash -), - -combined_results as ( - select * from batch_interaction_counts where num_interactions = risk_free - union - select *, 0 as num_interactions, 0 as risk_free from no_interactions -) - -select tx_hash from combined_results \ No newline at end of file diff --git a/queries/dune_v2/trade_counts.sql b/queries/dune_v2/trade_counts.sql deleted file mode 100644 index 1c26a588..00000000 --- a/queries/dune_v2/trade_counts.sql +++ /dev/null @@ -1,8 +0,0 @@ --- V3 Query: https://dune.com/queries/1785586 -select - solver_address as solver, - sum(num_trades) as num_trades -from cow_protocol_ethereum.batches -where block_number between {{start_block}} and {{end_block}} -group by solver_address -order by num_trades desc, solver diff --git a/queries/orderbook/order_rewards.sql b/queries/orderbook/order_rewards.sql deleted file mode 100644 index e4d7d5b9..00000000 --- a/queries/orderbook/order_rewards.sql +++ /dev/null @@ -1,31 +0,0 @@ -with trade_hashes as (SELECT solver, - order_uid, - fee_amount, - settlement.tx_hash, - auction_id - FROM trades t - LEFT OUTER JOIN LATERAL ( - SELECT tx_hash, solver, tx_nonce, tx_from - FROM settlements s - WHERE s.block_number = t.block_number - AND s.log_index > t.log_index - ORDER BY s.log_index ASC - LIMIT 1 - ) AS settlement ON true - join auction_transaction - -- This join also eliminates overlapping - -- trades & settlements between barn and prod DB - on settlement.tx_from = auction_transaction.tx_from - and settlement.tx_nonce = auction_transaction.tx_nonce - where block_number between {{start_block}} and {{end_block}}) - --- Most efficient column order for sorting would be having tx_hash or order_uid first -select concat('0x', encode(trade_hashes.order_uid, 'hex')) as order_uid, - concat('0x', encode(solver, 'hex')) as solver, - concat('0x', encode(tx_hash, 'hex')) as tx_hash, - coalesce(surplus_fee, 0) as surplus_fee, - coalesce(reward, 0.0) as amount -from trade_hashes - left outer join {{reward_table}} o - on trade_hashes.order_uid = o.order_uid - and trade_hashes.auction_id = o.auction_id; diff --git a/src/constants.py b/src/constants.py index 2cf79335..086e557f 100644 --- a/src/constants.py +++ b/src/constants.py @@ -52,3 +52,10 @@ # Real Web3 Instance web3 = Web3(Web3.HTTPProvider(NODE_URL)) + +RECOGNIZED_BONDING_POOLS = [ + "('0x8353713b6D2F728Ed763a04B886B16aAD2b16eBD', 'Gnosis', " + "'0x6c642cafcbd9d8383250bb25f67ae409147f78b2')", + "('0x5d4020b9261F01B6f8a45db929704b0Ad6F5e9E6', 'CoW Services', " + "'0x423cec87f19f0778f549846e0801ee267a917935')", +] diff --git a/src/fetch/cow_rewards.py b/src/fetch/cow_rewards.py deleted file mode 100644 index 5505b862..00000000 --- a/src/fetch/cow_rewards.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Fetching Per Order Rewards from Production and Staging Database""" -from pandas import DataFrame -from web3 import Web3 - - -def map_reward(amount: float, risk_free: bool) -> float: - """ - Converts orderbook rewards based on additional information of "risk_free" batches - - risk-free are batches containing only user and liquidity orders (i.e. no AMM interactions), - """ - if amount > 0 and risk_free: - # Risk Free User Orders that are not contained in unsafe batches 37 COW tokens. - return 37.0 - return amount - - -def aggregate_orderbook_rewards( - per_order_df: DataFrame, risk_free_transactions: set[str] -) -> DataFrame: - """ - Takes rewards per order and adjusts them based on whether they were part of - a risk-free settlement or not. After the new amount mapping is complete, - the results are aggregated by solver as a sum of amounts and additional - "transfer" related metadata is appended. The aggregated dataframe is returned. - """ - per_order_df["amount"] = per_order_df[["amount", "tx_hash"]].apply( - lambda x: map_reward( - amount=x.amount, - risk_free=x.tx_hash in risk_free_transactions, - ), - axis=1, - ) - result_agg = ( - per_order_df.groupby("solver")["amount"].agg(["count", "sum"]).reset_index() - ) - del per_order_df # We don't need this anymore! - # Add token address to each column - result_agg["token_address"] = "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB" - # Rename columns to align with "Transfer" Object - result_agg = result_agg.rename( - columns={"sum": "amount", "count": "num_trades", "solver": "receiver"} - ) - # Convert float amounts to WEI - result_agg["amount"] = result_agg["amount"].apply( - lambda x: int(Web3().to_wei(x, "ether")) - ) - return result_agg diff --git a/src/fetch/dune.py b/src/fetch/dune.py index 2b0d5f1c..3b0aad44 100644 --- a/src/fetch/dune.py +++ b/src/fetch/dune.py @@ -1,20 +1,14 @@ """All Dune related query fetching is defined here in the DuneFetcherClass""" from typing import Optional -import pandas as pd from dune_client.client import DuneClient from dune_client.query import Query from dune_client.types import QueryParameter, DuneRecord -from src.fetch.cow_rewards import aggregate_orderbook_rewards +from src.constants import RECOGNIZED_BONDING_POOLS from src.fetch.token_list import get_trusted_tokens from src.logger import set_log from src.models.accounting_period import AccountingPeriod -from src.models.slippage import SplitSlippages -from src.models.split_transfers import SplitTransfers -from src.models.transfer import Transfer -from src.models.vouch import RECOGNIZED_BONDING_POOLS, parse_vouches -from src.pg_client import DualEnvDataframe from src.queries import QUERIES, QueryData from src.utils.print_store import PrintStore, Category @@ -48,8 +42,9 @@ def _period_params(self) -> list[QueryParameter]: """Easier access to these parameters.""" return self.period.as_query_params() + @staticmethod def _parameterized_query( - self, query_data: QueryData, params: list[QueryParameter] + query_data: QueryData, params: list[QueryParameter] ) -> Query: return query_data.with_params(params) @@ -85,66 +80,6 @@ def get_block_interval(self) -> tuple[str, str]: assert len(results) == 1, "Block Interval Query should return only 1 result!" return str(results[0]["start_block"]), str(results[0]["end_block"]) - def get_eth_spent(self) -> list[Transfer]: - """ - Fetches ETH spent on successful settlements by all solvers during `period` - """ - results = self._get_query_results( - self._parameterized_query(QUERIES["ETH_SPENT"], self._period_params()) - ) - return [Transfer.from_dict(t) for t in results] - - def get_risk_free_batches(self) -> set[str]: - """Fetches Risk Free Batches from Dune""" - results = self._get_query_results( - self._parameterized_query( - QUERIES["RISK_FREE_BATCHES"], self._period_params() - ) - ) - return {row["tx_hash"].lower() for row in results} - - def get_trade_counts(self) -> list[DuneRecord]: - """Fetches Trade Counts for Period""" - return self._get_query_results( - query=self._parameterized_query( - query_data=QUERIES["TRADE_COUNT"], - params=[ - QueryParameter.text_type("start_block", self.start_block), - QueryParameter.text_type("end_block", self.end_block), - ], - ) - ) - - def get_cow_rewards(self) -> list[Transfer]: - """ - Fetches COW token rewards from orderbook database returning a list of Transfers - """ - print( - f"Fetching CoW Rewards for block interval {self.start_block}, {self.end_block}" - ) - per_order_df = DualEnvDataframe.get_orderbook_rewards( - self.start_block, self.end_block - ) - cow_rewards_df = aggregate_orderbook_rewards( - per_order_df, - risk_free_transactions=self.get_risk_free_batches(), - ) - - # Validation of results - using characteristics of results from two sources. - trade_counts = self.get_trade_counts() - # Number of trades per solver retrieved from orderbook agrees ethereum events. - duplicates = pd.concat( - [ - pd.DataFrame(trade_counts), - cow_rewards_df[["receiver", "num_trades"]].rename( - columns={"receiver": "solver"} - ), - ] - ).drop_duplicates(keep=False) - - assert len(duplicates) == 0, f"solver sets disagree: {duplicates}" - return Transfer.from_dataframe(cow_rewards_df) - def get_vouches(self) -> list[DuneRecord]: """ Fetches & Returns Parsed Results for VouchRegistry query. @@ -187,18 +122,3 @@ def get_period_slippage(self, job_id: Optional[str] = None) -> list[DuneRecord]: ), job_id, ) - - def get_transfers(self) -> list[Transfer]: - """Fetches and returns slippage-adjusted Transfers for solver reimbursement""" - # TODO - fetch these three results asynchronously! - reimbursements = self.get_eth_spent() - rewards = self.get_cow_rewards() - split_transfers = SplitTransfers( - period=self.period, - mixed_transfers=reimbursements + rewards, - log_saver=self.log_saver, - ) - return split_transfers.process( - slippages=SplitSlippages.from_data_set(self.get_period_slippage()), - cow_redirects=parse_vouches(self.get_vouches()), - ) diff --git a/src/fetch/payouts.py b/src/fetch/payouts.py index 92fdaaf9..949c2583 100644 --- a/src/fetch/payouts.py +++ b/src/fetch/payouts.py @@ -336,7 +336,7 @@ def construct_payout_dataframe( return merged_df -def post_cip20_payouts( +def construct_payouts( dune: DuneFetcher, orderbook: MultiInstanceDBFetcher ) -> list[Transfer]: """Workflow of solver reward payout logic post-CIP20""" diff --git a/src/fetch/transfer_file.py b/src/fetch/transfer_file.py index 7b0677e4..a8caf438 100644 --- a/src/fetch/transfer_file.py +++ b/src/fetch/transfer_file.py @@ -22,7 +22,7 @@ FILE_OUT_DIR, DOCS_URL, ) -from src.fetch.payouts import post_cip20_payouts +from src.fetch.payouts import construct_payouts from src.models.accounting_period import AccountingPeriod from src.models.transfer import Transfer, CSVTransfer from src.multisend import post_multisend, prepend_unwrap_if_necessary @@ -108,18 +108,13 @@ def auto_propose( category=Category.GENERAL, ) - if args.pre_cip20: - payout_transfers = dune.get_transfers() - Transfer.sort_list(payout_transfers) - - else: - payout_transfers = post_cip20_payouts( - args.dune, - orderbook=MultiInstanceDBFetcher( - [os.environ["PROD_DB_URL"], os.environ["BARN_DB_URL"]] - ), - ) - + payout_transfers = construct_payouts( + args.dune, + orderbook=MultiInstanceDBFetcher( + [os.environ["PROD_DB_URL"], os.environ["BARN_DB_URL"]] + ), + ) + Transfer.sort_list(payout_transfers) payout_transfers = list( filter( lambda payout: payout.amount_wei > args.min_transfer_amount_wei, diff --git a/src/models/overdraft.py b/src/models/overdraft.py index bfb0eaad..9495cabe 100644 --- a/src/models/overdraft.py +++ b/src/models/overdraft.py @@ -6,9 +6,6 @@ from dune_client.types import Address from src.models.accounting_period import AccountingPeriod -from src.models.slippage import SolverSlippage -from src.models.token import TokenType -from src.models.transfer import Transfer @dataclass @@ -23,22 +20,6 @@ class Overdraft: name: str wei: int - @classmethod - def from_objects( - cls, transfer: Transfer, slippage: SolverSlippage, period: AccountingPeriod - ) -> Overdraft: - """Constructs an overdraft instance based on Transfer & Slippage""" - assert transfer.recipient == slippage.solver_address - assert transfer.token_type == TokenType.NATIVE - overdraft = transfer.amount_wei + slippage.amount_wei - assert overdraft < 0, "This is why we are here." - return cls( - period=period, - name=slippage.solver_name, - account=slippage.solver_address, - wei=abs(overdraft), - ) - @property def eth(self) -> float: """Returns amount in units""" diff --git a/src/models/period_totals.py b/src/models/period_totals.py deleted file mode 100644 index bfe81e14..00000000 --- a/src/models/period_totals.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Script to query and display total funds distributed for specified accounting period. -""" -from dataclasses import dataclass - -from src.models.accounting_period import AccountingPeriod - - -@dataclass -class PeriodTotals: - """Total amount reimbursed for accounting period""" - - period: AccountingPeriod - execution_cost_eth: int - cow_rewards: int - realized_fees_eth: int diff --git a/src/models/slippage.py b/src/models/slippage.py deleted file mode 100644 index a177de27..00000000 --- a/src/models/slippage.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Dataclass for SolverSlippage along with Split Slippage class that handles a collection -of SolverSlippage objects. -""" - -from __future__ import annotations - -from dataclasses import dataclass - -from dune_client.types import Address - - -@dataclass -class SolverSlippage: - """Total amount reimbursed for accounting period""" - - solver_address: Address - solver_name: str - # ETH amount (in WEI) to be deducted from Solver reimbursement - amount_wei: int - - @classmethod - def from_dict(cls, obj: dict[str, str]) -> SolverSlippage: - """Converts Dune data dict to object with types""" - return cls( - solver_address=Address(obj["solver_address"]), - solver_name=obj["solver_name"], - amount_wei=int(obj["eth_slippage_wei"]), - ) - - -@dataclass -class SplitSlippages: - """Basic class to store the output of slippage fetching""" - - solvers_with_negative_total: list[SolverSlippage] - solvers_with_positive_total: list[SolverSlippage] - - def __init__(self) -> None: - self.solvers_with_negative_total = [] - self.solvers_with_positive_total = [] - - @classmethod - def from_data_set(cls, data_set: list[dict[str, str]]) -> SplitSlippages: - """Constructs an object based on provided dataset""" - results = cls() - for row in data_set: - results.append(slippage=SolverSlippage.from_dict(row)) - return results - - def append(self, slippage: SolverSlippage) -> None: - """Appends the Slippage to the appropriate half based on signature of amount""" - if slippage.amount_wei < 0: - self.solvers_with_negative_total.append(slippage) - else: - self.solvers_with_positive_total.append(slippage) - - def __len__(self) -> int: - return len(self.solvers_with_negative_total) + len( - self.solvers_with_positive_total - ) - - def sum_negative(self) -> int: - """Returns total negative slippage""" - return sum(neg.amount_wei for neg in self.solvers_with_negative_total) - - def sum_positive(self) -> int: - """Returns total positive slippage""" - return sum(pos.amount_wei for pos in self.solvers_with_positive_total) diff --git a/src/models/split_transfers.py b/src/models/split_transfers.py deleted file mode 100644 index ecbdebeb..00000000 --- a/src/models/split_transfers.py +++ /dev/null @@ -1,191 +0,0 @@ -""" -The Split Transfer Class is responsible for processing a mixed list of transfers. -It exposes only one public method "process" which ensures that its -internal methods are called in the appropriate order. -""" -from __future__ import annotations - -from datetime import timedelta - -from dune_client.types import Address - -from src.models.vouch import Vouch -from src.models.accounting_period import AccountingPeriod -from src.models.overdraft import Overdraft -from src.models.slippage import SolverSlippage, SplitSlippages -from src.models.token import TokenType -from src.models.transfer import Transfer -from src.fetch.prices import eth_in_token, TokenId, token_in_eth -from src.utils.dataset import index_by -from src.utils.print_store import Category, PrintStore - - -# pylint: disable=too-few-public-methods -class SplitTransfers: - """ - This class keeps the ERC20 and NATIVE token transfers Split. - Technically we should have two additional classes one for each token type. - """ - - def __init__( - self, - period: AccountingPeriod, - mixed_transfers: list[Transfer], - log_saver: PrintStore, - ): - self.log_saver = log_saver - self.period = period - self.unprocessed_native = [] - self.unprocessed_cow = [] - for transfer in mixed_transfers: - if transfer.token_type == TokenType.NATIVE: - self.unprocessed_native.append(transfer) - elif transfer.token_type == TokenType.ERC20: - self.unprocessed_cow.append(transfer) - else: - raise ValueError(f"Invalid token type! {transfer.token_type}") - # Initialize empty overdraft - self.overdrafts: dict[Address, Overdraft] = {} - self.eth_transfers: list[Transfer] = [] - self.cow_transfers: list[Transfer] = [] - - def _process_native_transfers( - self, indexed_slippage: dict[Address, SolverSlippage] - ) -> int: - """ - Draining the `unprocessed_native` (ETH) transfers into processed - versions as `eth_transfers`. Processing adjusts for negative slippage by deduction. - Returns: total negative slippage - """ - penalty_total = 0 - while self.unprocessed_native: - transfer = self.unprocessed_native.pop(0) - solver = transfer.recipient - try: - slippage: SolverSlippage = indexed_slippage.pop(solver) - assert ( - slippage.amount_wei < 0 - ), f"Expected negative slippage! Got {slippage}" - try: - transfer.add_slippage(slippage, self.log_saver) - penalty_total += slippage.amount_wei - except ValueError as err: - name, address = slippage.solver_name, slippage.solver_address - self.log_saver.print( - f"Slippage for {address} ({name}) exceeds reimbursement: {err}\n" - f"Excluding payout and appending excess to overdraft", - category=Category.OVERDRAFT, - ) - self.overdrafts[solver] = Overdraft.from_objects( - transfer, slippage, self.period - ) - # Deduct entire transfer value. - penalty_total -= transfer.amount_wei - continue - except KeyError: - pass - self.eth_transfers.append(transfer) - - if indexed_slippage: - # This is the case when solver had Negative Payment - # and did not appear in the list of unprocessed_native transfers - # slippages were expected drained! - # TODO - Deal with this! - raise ValueError( - "This is a scenario where solver has no payment but also has negative slippage!" - ) - - return penalty_total - - def _process_rewards( - self, - redirect_map: dict[Address, Vouch], - positive_slippage: list[SolverSlippage], - ) -> int: - """ - Draining the `unprocessed_cow` (COW) transfers into processed versions - as `cow_transfers`. Processing accounts for overdraft and positive slippage. - Returns: total positive slippage - """ - price_day = self.period.end - timedelta(days=1) - while self.unprocessed_cow: - transfer = self.unprocessed_cow.pop(0) - solver = transfer.recipient - # Remove the element if it exists (assuming it won't have to be reinserted) - overdraft = self.overdrafts.pop(solver, None) - if overdraft is not None: - cow_deduction = eth_in_token(TokenId.COW, overdraft.wei, price_day) - self.log_saver.print( - f"Deducting {cow_deduction} COW from reward for {solver}", - category=Category.OVERDRAFT, - ) - transfer.amount_wei -= cow_deduction - if transfer.amount_wei < 0: - self.log_saver.print( - "Overdraft exceeds COW reward! " - "Excluding reward and updating overdraft", - category=Category.OVERDRAFT, - ) - overdraft.wei = token_in_eth( - TokenId.COW, abs(transfer.amount_wei), price_day - ) - # Reinsert since there is still an amount owed. - self.overdrafts[solver] = overdraft - continue - transfer.try_redirect(redirect_map, self.log_saver) - self.cow_transfers.append(transfer) - # We do not need to worry about any controversy between overdraft - # and positive slippage adjustments, because positive/negative slippage - # is disjoint between solvers. - total_positive_slippage = 0 - while positive_slippage: - slippage = positive_slippage.pop() - assert ( - slippage.amount_wei > 0 - ), f"Expected positive slippage got {slippage.amount_wei}" - total_positive_slippage += slippage.amount_wei - slippage_transfer = Transfer.from_slippage(slippage) - slippage_transfer.try_redirect(redirect_map, self.log_saver) - self.eth_transfers.append(slippage_transfer) - return total_positive_slippage - - def process( - self, - slippages: SplitSlippages, - cow_redirects: dict[Address, Vouch], - ) -> list[Transfer]: - """ - This is the public interface to construct the final transfer file based on - raw (unpenalized) results, positive, negative slippage, rewards and overdrafts. - It is very important that the native token transfers are processed first, - so that any overdraft from slippage can be carried over and deducted from - the COW rewards. - """ - total_penalty = self._process_native_transfers( - indexed_slippage=index_by( - slippages.solvers_with_negative_total, "solver_address" - ) - ) - self.log_saver.print( - f"Total Negative Slippage (ETH): {total_penalty / 10**18:.4f}", - category=Category.TOTALS, - ) - # Note that positive and negative slippage is DISJOINT. - # So no overdraft computations will overlap with the positive slippage perturbations. - total_positive_slippage = self._process_rewards( - cow_redirects, - positive_slippage=slippages.solvers_with_positive_total, - ) - self.log_saver.print( - f"Total Positive Slippage (ETH): {total_positive_slippage / 10**18:.4f}", - category=Category.TOTALS, - ) - if self.overdrafts: - accounts_owing = "\n".join(map(str, self.overdrafts.values())) - self.log_saver.print( - f"Additional owed\n {accounts_owing}", category=Category.OVERDRAFT - ) - return self.cow_transfers + self.eth_transfers - - -# pylint: enable=too-few-public-methods diff --git a/src/models/transfer.py b/src/models/transfer.py index c6c03e80..df26e164 100644 --- a/src/models/transfer.py +++ b/src/models/transfer.py @@ -14,10 +14,7 @@ from web3 import Web3 from src.abis.load import erc20 -from src.models.slippage import SolverSlippage from src.models.token import TokenType, Token -from src.models.vouch import Vouch -from src.utils.print_store import Category, PrintStore ERC20_CONTRACT = erc20() @@ -148,20 +145,6 @@ def amount(self) -> float: assert self.token is not None return self.amount_wei / int(10**self.token.decimals) - def add_slippage(self, slippage: SolverSlippage, log_saver: PrintStore) -> None: - """Adds Adjusts Transfer amount by Slippage amount""" - assert self.recipient == slippage.solver_address, "receiver != solver" - adjustment = slippage.amount_wei - log_saver.print( - f"Deducting slippage for solver {self.recipient}" - f"by {adjustment / 10 ** 18:.5f} ({slippage.solver_name})", - category=Category.SLIPPAGE, - ) - new_amount = self.amount_wei + adjustment - if new_amount <= 0: - raise ValueError(f"Invalid adjustment {self} by {adjustment / 10 ** 18}") - self.amount_wei = new_amount - def merge(self, other: Transfer) -> Transfer: """ Merge two transfers (acts like addition) if all fields except amount are equal, @@ -219,42 +202,6 @@ def __str__(self) -> str: ) raise ValueError(f"Invalid Token Type {self.token_type}") - def try_redirect( - self, redirects: dict[Address, Vouch], log_saver: PrintStore - ) -> None: - """ - Redirects Transfers via Address => Vouch.reward_target - This function modifies self! - """ - recipient = self.recipient - if recipient in redirects: - # Redirect COW rewards to reward target specific by VouchRegistry - redirect_address = redirects[recipient].reward_target - log_saver.print( - f"Redirecting {recipient} Transfer of {self.amount} to {redirect_address}", - category=Category.ETH_REDIRECT - if self.token is None - else Category.COW_REDIRECT, - ) - # This is the only place where recipient can be overwritten - # sort_key is not updated here because sorted transfers should be - # grouped by "initial recipient" before they were redirected. - # This is a business requirement for making non-consolidated - # multisend transaction validation more visually straight-forward. - self._recipient = redirect_address - - @classmethod - def from_slippage(cls, slippage: SolverSlippage) -> Transfer: - """ - Slippage is always in ETH, so this converts - slippage into an ETH Transfer with Null token address - """ - return cls( - token=None, - recipient=slippage.solver_address, - amount_wei=slippage.amount_wei, - ) - @staticmethod def sort_list(transfer_list: list[Transfer]) -> None: """ diff --git a/src/models/vouch.py b/src/models/vouch.py deleted file mode 100644 index 480b5702..00000000 --- a/src/models/vouch.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Script to query and display total funds distributed for specified accounting period. -""" -from dataclasses import dataclass - -from dune_client.types import Address - -from src.utils.dataset import index_by - -RECOGNIZED_BONDING_POOLS = [ - "('0x8353713b6D2F728Ed763a04B886B16aAD2b16eBD', 'Gnosis', " - "'0x6c642cafcbd9d8383250bb25f67ae409147f78b2')", - "('0x5d4020b9261F01B6f8a45db929704b0Ad6F5e9E6', 'CoW Services', " - "'0x423cec87f19f0778f549846e0801ee267a917935')", -] - - -@dataclass -class Vouch: - """Data triplet linking solvers to bonding pools and COW reward destination""" - - solver: Address - reward_target: Address - bonding_pool: Address - - -def parse_vouches(raw_data: list[dict[str, str]]) -> dict[Address, Vouch]: - """Parses the Dune Response of VouchRegistry query""" - result_list = [ - Vouch( - solver=Address(rec["solver"]), - reward_target=Address(rec["reward_target"]), - bonding_pool=Address(rec["pool"]), - ) - for rec in raw_data - ] - # Indexing here ensures the solver's returned from Dune are unique! - return index_by(result_list, "solver") diff --git a/src/pg_client.py b/src/pg_client.py index e2934699..f3a59c84 100644 --- a/src/pg_client.py +++ b/src/pg_client.py @@ -1,105 +1,15 @@ """Basic client for connecting to postgres database with login credentials""" from __future__ import annotations -import os -from dataclasses import dataclass -from enum import Enum import pandas as pd -from dotenv import load_dotenv from pandas import DataFrame -from sqlalchemy.exc import ProgrammingError from sqlalchemy import create_engine from sqlalchemy.engine import Engine from src.utils.query_file import open_query -class OrderbookEnv(Enum): - """ - Enum for distinguishing between CoW Protocol's staging and production environment - """ - - BARN = "BARN" - PROD = "PROD" - - def __str__(self) -> str: - return str(self.value) - - -@dataclass -class DualEnvDataframe: - """ - A pair of Dataframes primarily intended to store query results - from production and staging orderbook databases - """ - - barn: DataFrame - prod: DataFrame - - @staticmethod - def _pg_engine(db_env: OrderbookEnv) -> Engine: - """Returns a connection to postgres database""" - load_dotenv() - host = os.environ[f"{db_env}_ORDERBOOK_HOST"] - port = os.environ[f"{db_env}_ORDERBOOK_PORT"] - database = os.environ[f"{db_env}_ORDERBOOK_DB"] - user = os.environ[f"{db_env}_ORDERBOOK_USER"] - password = os.environ[f"{db_env}_ORDERBOOK_PASSWORD"] - db_string = f"postgresql+psycopg2://{user}:{password}@{host}:{port}/{database}" - return create_engine(db_string) - - @classmethod - def _exec_query(cls, query: str, engine: Engine) -> DataFrame: - # TODO - once both environments have been migrated, this will no longer be necessary. - try: - # New Query - return pd.read_sql( - sql=query.replace("{{reward_table}}", "order_execution"), con=engine - ) - except ProgrammingError: - # Use Old Query - # Unfortunately it appears impossible to capture the Base Error: - # psycopg2.errors.UndefinedTable - # But we know what it is. - return pd.read_sql( - sql=query.replace("{{reward_table}}", "order_rewards"), con=engine - ) - - @classmethod - def from_query(cls, query: str) -> DualEnvDataframe: - """Fetch results of DB query on both prod and barn and returns the results as a pair""" - return cls( - barn=cls._exec_query(query, cls._pg_engine(OrderbookEnv.BARN)), - prod=cls._exec_query(query, cls._pg_engine(OrderbookEnv.PROD)), - ) - - def merge(self) -> DataFrame: - """Merges prod and barn dataframes via concatenation""" - # TODO - verify generic disjointness here. - return pd.concat([self.prod, self.barn]) - - @classmethod - def get_orderbook_rewards(cls, start_block: str, end_block: str) -> DataFrame: - """ - Fetches and validates Orderbook Reward DataFrame as concatenation from Prod and Staging DB - """ - cow_reward_query = ( - open_query("orderbook/order_rewards.sql") - .replace("{{start_block}}", start_block) - .replace("{{end_block}}", end_block) - ) - dual_df = cls.from_query(cow_reward_query) - - # Solvers do not appear in both environments! - # TODO - move this assertion into merge: - # https://github.com/cowprotocol/solver-rewards/issues/125 - assert set(dual_df.prod.solver).isdisjoint( - set(dual_df.barn.solver) - ), "solver overlap!" - return dual_df.merge() - - class MultiInstanceDBFetcher: """ Allows identical query execution on multiple db instances (merging results). diff --git a/src/queries.py b/src/queries.py index bf2d40df..9c09bf45 100644 --- a/src/queries.py +++ b/src/queries.py @@ -30,31 +30,16 @@ def with_params(self, params: list[QueryParameter]) -> Query: QUERIES = { - "TRADE_COUNT": QueryData( - name="Trade Counts", - q_id=1785586, - filepath="dune_trade_counts.sql", - ), "PERIOD_BLOCK_INTERVAL": QueryData( name="Block Interval for Accounting Period", filepath="period_block_interval.sql", q_id=1541504, ), - "RISK_FREE_BATCHES": QueryData( - name="Risk Free Batches", - filepath="risk_free_batches.sql", - q_id=1788438, - ), "VOUCH_REGISTRY": QueryData( name="Vouch Registry", filepath="vouch_registry.sql", q_id=1541516, ), - "ETH_SPENT": QueryData( - name="ETH Reimbursement", - filepath="eth_spent.sql", - q_id=1320174, - ), "PERIOD_SLIPPAGE": QueryData( name="Solver Slippage for Period", filepath="period_slippage.sql", diff --git a/src/utils/script_args.py b/src/utils/script_args.py index d4bd95b8..91461ad6 100644 --- a/src/utils/script_args.py +++ b/src/utils/script_args.py @@ -17,7 +17,6 @@ class ScriptArgs: dune: DuneFetcher post_tx: bool dry_run: bool - pre_cip20: bool consolidate_transfers: bool min_transfer_amount_wei: int @@ -42,13 +41,6 @@ def generic_script_init(description: str) -> ScriptArgs: "(requires valid env var `PROPOSER_PK`)", default=False, ) - parser.add_argument( - "--pre-cip20", - type=bool, - help="Flag payout should be made according to pre or post CIP-20. " - "Default is set to the current reward scheme", - default=False, - ) parser.add_argument( "--consolidate-transfers", type=bool, @@ -64,6 +56,7 @@ def generic_script_init(description: str) -> ScriptArgs: "Primarily intended for deployment in staging environment.", default=False, ) + # TODO: this should be per token (like list[tuple[Token,minAmount]]) parser.add_argument( "--min-transfer-amount-wei", type=int, @@ -76,7 +69,6 @@ def generic_script_init(description: str) -> ScriptArgs: dune=DuneClient(os.environ["DUNE_API_KEY"]), period=AccountingPeriod(args.start), ), - pre_cip20=args.pre_cip20, post_tx=args.post_tx, dry_run=args.dry_run, consolidate_transfers=args.consolidate_transfers, diff --git a/tests/e2e/test_get_transfers.py b/tests/e2e/test_get_transfers.py deleted file mode 100644 index e53fea89..00000000 --- a/tests/e2e/test_get_transfers.py +++ /dev/null @@ -1,38 +0,0 @@ -import os -import unittest - -from dotenv import load_dotenv -from dune_client.client import DuneClient - -from src.fetch.dune import DuneFetcher -from src.models.accounting_period import AccountingPeriod - - -class MyTestCase(unittest.TestCase): - def setUp(self) -> None: - load_dotenv() - self.fetcher = DuneFetcher( - DuneClient(os.environ["DUNE_API_KEY"]), - AccountingPeriod("2022-10-18"), - ) - - def test_get_eth_spent(self): - self.fetcher.period = AccountingPeriod("2022-09-20") - eth_transfers = self.fetcher.get_eth_spent() - self.assertAlmostEqual( - sum(t.amount_wei for t in eth_transfers), - 16745457506431162000, # cf: https://dune.com/queries/1323288 - delta=5 * 10**4, # WEI - ) - - def test_get_cow_rewards(self): - self.fetcher.period = AccountingPeriod("2022-10-18", length_days=5) - print(f"Check out results at: {self.fetcher.period.dashboard_url()}") - try: - self.fetcher.get_cow_rewards() - except AssertionError as err: - self.fail(f"get_cow_rewards failed with {err}") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/e2e/test_per_batch_rewards.py b/tests/e2e/test_per_batch_rewards.py deleted file mode 100644 index d04c9c12..00000000 --- a/tests/e2e/test_per_batch_rewards.py +++ /dev/null @@ -1,76 +0,0 @@ -import os -import unittest -import pandas as pd -from dotenv import load_dotenv -from dune_client.client import DuneClient - -from src.fetch.cow_rewards import map_reward -from src.fetch.dune import DuneFetcher -from src.models.accounting_period import AccountingPeriod -from src.pg_client import DualEnvDataframe - - -def reward_for_tx(df: pd.DataFrame, tx_hash: str, risk_free: bool) -> tuple[int, float]: - print(df, tx_hash, risk_free) - batch_subset = df.loc[df["tx_hash"] == tx_hash] - order_rewards = batch_subset[["amount"]].apply( - lambda x: map_reward(x.amount, risk_free), - axis=1, - ) - return order_rewards.size, order_rewards.sum() - - -class TestPerBatchRewards(unittest.TestCase): - """ - These tests aren't actually necessary because their logic is captured by a unit test - tests/unit/test_reward_aggregation.py - cf: https://github.com/cowprotocol/solver-rewards/pull/107#issuecomment-1288566854 - """ - - def setUp(self) -> None: - load_dotenv() - dune = DuneFetcher( - DuneClient(os.environ["DUNE_API_KEY"]), - AccountingPeriod("2022-10-18"), - ) - start_block, end_block = dune.get_block_interval() - - self.rewards_df = DualEnvDataframe.get_orderbook_rewards(start_block, end_block) - self.risk_free_batches = dune.get_risk_free_batches() - - def test_buffer_trade(self): - tx_hash = "0x6b6181e95ae837376dd15adbe7801bffffee639dbc8f18b918ace9645a5c1be2" - self.assertEqual( - reward_for_tx( - self.rewards_df, - tx_hash, - tx_hash in self.risk_free_batches, - ), - (1, 37.0), - ) - - def test_perfect_cow_with_native_liquidity(self): - tx_hash = "0x72e4c54e9c9dc2ee2a09dd242bf80abc39d122af0813ff4d570d3ce04eea8468" - self.assertEqual( - reward_for_tx( - self.rewards_df, - tx_hash, - tx_hash in self.risk_free_batches, - ), - (2, 37.0), - ) - - def test_perfect_cow_with_foreign_liquidity(self): - tx_hash = "0x43bfe76d590966c7539f1ea0bb7989edc1289f989eaf8d84589c3508c5066c2c" - self.assertEqual( - reward_for_tx( - self.rewards_df, - tx_hash, - tx_hash in self.risk_free_batches, - ), - (2, 37.0), - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/e2e/test_transfer_file.py b/tests/e2e/test_transfer_file.py deleted file mode 100644 index c814a998..00000000 --- a/tests/e2e/test_transfer_file.py +++ /dev/null @@ -1,79 +0,0 @@ -import unittest - -from dune_client.types import Address - -from src.constants import COW_TOKEN_ADDRESS -from src.fetch.transfer_file import Transfer -from src.models.accounting_period import AccountingPeriod -from src.models.overdraft import Overdraft -from src.models.slippage import SplitSlippages -from src.models.split_transfers import SplitTransfers -from src.models.token import Token -from src.utils.print_store import PrintStore - -ONE_ETH = 10**18 - - -# TODO - mock the price feed so that this test doesn't require API call. -class TestPrices(unittest.TestCase): - def test_process_transfers(self): - period = AccountingPeriod("2022-06-14") - barn_zerox = Address("0xde786877a10dbb7eba25a4da65aecf47654f08ab") - other_solver = Address("0x" + "1" * 40) - cow_token = Token(COW_TOKEN_ADDRESS) - mixed_transfers = [ - Transfer( - token=None, - recipient=barn_zerox, - amount_wei=185360274773133130, - ), - Transfer(token=None, recipient=other_solver, amount_wei=1 * ONE_ETH), - Transfer(token=cow_token, recipient=barn_zerox, amount_wei=600 * ONE_ETH), - Transfer( - token=cow_token, recipient=other_solver, amount_wei=2000 * ONE_ETH - ), - ] - slippages = SplitSlippages.from_data_set( - [ - # Barn Slippage - { - "eth_slippage_wei": -324697366789535540, - "solver_name": "barn-0x", - "solver_address": barn_zerox.address, - }, - # Other Slippage - { - "eth_slippage_wei": -11 * 10**17, - "solver_name": "Other Solver", - "solver_address": other_solver.address, - }, - ] - ) - cow_redirects = {} - - accounting = SplitTransfers(period, mixed_transfers, PrintStore()) - - transfers = accounting.process(slippages, cow_redirects) - # The only remaining transfer is the other_solver's COW reward. - self.assertEqual( - transfers, - [ - Transfer( - token=cow_token, - recipient=other_solver, - amount_wei=845094377028141056000, - ) - ], - ) - # barn_zerox still has outstanding overdraft - self.assertEqual( - accounting.overdrafts, - {barn_zerox: Overdraft(period, barn_zerox, "barn-0x", 87384794957180304)}, - ) - # All unprocessed entries have been processed. - self.assertEqual(accounting.unprocessed_cow, []) - self.assertEqual(accounting.unprocessed_native, []) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 167bd74d..9a33d53c 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -8,11 +8,9 @@ from src.abis.load import erc20 from src.constants import COW_TOKEN_ADDRESS -from src.models.slippage import SolverSlippage from src.fetch.transfer_file import Transfer from src.models.accounting_period import AccountingPeriod from src.models.token import Token -from src.models.vouch import Vouch from src.utils.print_store import PrintStore from tests.queries.test_internal_trades import TransferType @@ -48,38 +46,6 @@ def setUp(self) -> None: self.token_1 = Token(Address.from_int(1), 18) self.token_2 = Token(Address.from_int(2), 18) - def test_add_slippage(self): - solver = Address.zero() - transfer = Transfer( - token=None, - recipient=solver, - amount_wei=ONE_ETH, - ) - positive_slippage = SolverSlippage( - solver_name="Test Solver", solver_address=solver, amount_wei=ONE_ETH // 2 - ) - negative_slippage = SolverSlippage( - solver_name="Test Solver", - solver_address=solver, - amount_wei=-ONE_ETH // 2, - ) - transfer.add_slippage(positive_slippage, PrintStore()) - self.assertAlmostEqual(transfer.amount, 1.5, delta=0.0000000001) - transfer.add_slippage(negative_slippage, PrintStore()) - self.assertAlmostEqual(transfer.amount, 1.0, delta=0.0000000001) - - overdraft_slippage = SolverSlippage( - solver_name="Test Solver", solver_address=solver, amount_wei=-2 * ONE_ETH - ) - - with self.assertRaises(ValueError) as err: - transfer.add_slippage(overdraft_slippage, PrintStore()) - self.assertEqual( - str(err.exception), - f"Invalid adjustment {transfer} " - f"by {overdraft_slippage.amount_wei / 10**18}", - ) - def test_basic_consolidation(self): recipients = [ Address.from_int(0), @@ -370,23 +336,6 @@ def test_merge_with_redirects(self): str(err.exception), ) - def test_receiver_error(self): - transfer = Transfer( - token=None, - recipient=Address.from_int(1), - amount_wei=1 * ONE_ETH, - ) - with self.assertRaises(AssertionError) as err: - transfer.add_slippage( - SolverSlippage( - solver_name="Test Solver", - solver_address=Address.from_int(2), - amount_wei=0, - ), - PrintStore(), - ) - self.assertEqual(err, "receiver != solver") - def test_from_dict(self): receiver = Address.from_int(1) self.assertEqual( @@ -528,45 +477,6 @@ def test_summarize(self): "Total ETH Funds needed: 123.4568\nTotal COW Funds needed: 10000000.0000\n", ) - def test_try_redirect(self): - """ - Test demonstrates that try_redirect works as expected for our use case. - However, it also demonstrates how bad it is to pass in an unstructured hashmap - that expects the keys to be equal the solver field of its values! - TODO - fix this strange error prone issue! - """ - dummy_print_store = PrintStore() - receiver = Address.from_int(1) - redirect = Address.from_int(2) - # Try redirect elsewhere - t1 = Transfer(token=None, amount_wei=1, recipient=receiver) - vouch_forward = Vouch( - bonding_pool=Address.zero(), reward_target=redirect, solver=receiver - ) - t1.try_redirect({vouch_forward.solver: vouch_forward}, dummy_print_store) - self.assertEqual(t1.recipient, redirect) - - vouch_reverse = Vouch( - bonding_pool=Address.zero(), reward_target=receiver, solver=redirect - ) - # Redirect back! - t1.try_redirect({vouch_reverse.solver: vouch_reverse}, dummy_print_store) - self.assertEqual(t1.recipient, receiver) - - # no action redirect. - another_address = Address.from_int(5) - t2 = Transfer(token=None, amount_wei=1, recipient=another_address) - disjoint_redirect_map = { - vouch_forward.solver: vouch_forward, - vouch_reverse.solver: vouch_reverse, - } - # This assertion implies we should expect t2 to remain unchanged after "try_redirect" - self.assertFalse(t2.recipient in disjoint_redirect_map.keys()) - t2.try_redirect(disjoint_redirect_map, dummy_print_store) - self.assertEqual( - t2, Transfer(token=None, amount_wei=1, recipient=another_address) - ) - def test_sorted_output(self): solver_1 = Address.from_int(1) solver_2 = Address.from_int(2) diff --git a/tests/unit/test_reward_aggregation.py b/tests/unit/test_reward_aggregation.py deleted file mode 100644 index 9047b5b7..00000000 --- a/tests/unit/test_reward_aggregation.py +++ /dev/null @@ -1,90 +0,0 @@ -import unittest - -from web3 import Web3 - -import pandas as pd - -from src.fetch.cow_rewards import aggregate_orderbook_rewards, map_reward - - -def to_wei(t) -> int: - return Web3().to_wei(t, "ether") - - -class MyTestCase(unittest.TestCase): - """ - This test is a mock dataset capturing the real data from - cf: https://github.com/cowprotocol/solver-rewards/pull/107#issuecomment-1288566854 - """ - - def test_aggregate_orderbook_rewards(self): - solvers = [ - "0x1", - "0x1", - "0x2", - "0x2", - "0x3", - "0x1", - "0x2", - "0x3", - "0x4", - "0x4", - "0x4", - ] - tx_hashes = [ - # Tx 0x001 is 0x72e4c54e9c9dc2ee2a09dd242bf80abc39d122af0813ff4d570d3ce04eea8468 - "0x001", - "0x001", - # Tx 0x002 is 0x43bfe76d590966c7539f1ea0bb7989edc1289f989eaf8d84589c3508c5066c2c - "0x002", - "0x002", - # Tx 0x003 is 0x6b6181e95ae837376dd15adbe7801bffffee639dbc8f18b918ace9645a5c1be2 - "0x003", - "0x004", - "0x005", - "0x006", - # Tx 0x007 0x82318dd23592f7ccba72fcad43c452c4c426d9e02c7cf3b1f9e7823a0c9a9fc0 - "0x007", - "0x007", - "0x007", - ] - amounts = [39, 0, 40, 0, 41, 50, 60, 70, 40, 50, 0] - surplus_fees = [None] * len(amounts) - orderbook_rewards = pd.DataFrame( - { - "solver": solvers, - "tx_hash": tx_hashes, - "surplus_fee": surplus_fees, - "amount": amounts, - } - ) - results = aggregate_orderbook_rewards( - orderbook_rewards, - risk_free_transactions={"0x001", "0x002", "0x003", "0x007"}, - ) - expected = pd.DataFrame( - { - "receiver": ["0x1", "0x2", "0x3", "0x4"], - "num_trades": [3, 3, 2, 3], - "amount": [to_wei(87), to_wei(97), to_wei(107), to_wei(74)], - "token_address": [ - "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", - "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", - "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", - "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", - ], - } - ) - print(expected) - print(results) - self.assertIsNone(pd.testing.assert_frame_equal(expected, results)) - - def test_map_reward(self): - self.assertEqual(map_reward(0, True), 0) - self.assertEqual(map_reward(1, True), 37) - self.assertEqual(map_reward(0, False), 0) - self.assertEqual(map_reward(1, False), 1) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/unit/test_split_transfer.py b/tests/unit/test_split_transfer.py deleted file mode 100644 index 2e9ab43c..00000000 --- a/tests/unit/test_split_transfer.py +++ /dev/null @@ -1,409 +0,0 @@ -import unittest - -from dune_client.types import Address - -from src.constants import COW_TOKEN_ADDRESS -from src.models.accounting_period import AccountingPeriod -from src.models.overdraft import Overdraft -from src.models.slippage import SolverSlippage, SplitSlippages -from src.models.split_transfers import SplitTransfers -from src.models.token import Token -from src.models.transfer import Transfer -from src.models.vouch import Vouch -from src.utils.print_store import PrintStore -from tests.unit.util_methods import redirected_transfer - -ONE_ETH = 10**18 - - -class TestSplitTransfers(unittest.TestCase): - def setUp(self) -> None: - self.period = AccountingPeriod("2023-06-14") - self.solver = Address("0xde786877a10dbb7eba25a4da65aecf47654f08ab") - self.solver_name = "solver_0" - self.redirect_map = { - self.solver: Vouch( - solver=self.solver, - reward_target=Address.from_int(2), - bonding_pool=Address.from_int(3), - ) - } - self.cow_token = Token(COW_TOKEN_ADDRESS) - - def construct_split_transfers_and_process( - self, - solvers: list[Address], - eth_amounts: list[int], - cow_rewards: list[int], - slippage_amounts: list[int], - redirects: dict[Address, Vouch], - ) -> SplitTransfers: - eth_transfers = [ - Transfer( - token=None, - recipient=solvers[i], - amount_wei=eth_amounts[i], - ) - for i in range(len(solvers)) - ] - cow_transfers = [ - Transfer( - token=self.cow_token, recipient=solvers[i], amount_wei=cow_rewards[i] - ) - for i in range(len(solvers)) - ] - accounting = SplitTransfers( - self.period, - mixed_transfers=eth_transfers + cow_transfers, - log_saver=PrintStore(), - ) - accounting.process( - slippages=SplitSlippages.from_data_set( - [ - { - "eth_slippage_wei": slippage_amounts[i], - "solver_address": solvers[i].address, - "solver_name": f"solver_{i}", - } - for i in range(len(slippage_amounts)) - ] - ), - cow_redirects=redirects, - ) - return accounting - - def test_process_native_transfers(self): - amount_of_transfer = 185360274773133130 - mixed_transfers = [ - Transfer( - token=None, - recipient=self.solver, - amount_wei=amount_of_transfer, - ), - Transfer( - token=self.cow_token, recipient=self.solver, amount_wei=600 * ONE_ETH - ), - ] - - slippage = SolverSlippage( - amount_wei=-amount_of_transfer - ONE_ETH, - solver_name=self.solver_name, - solver_address=self.solver, - ) - indexed_slippage = {self.solver: slippage} - accounting = SplitTransfers(self.period, mixed_transfers, PrintStore()) - - total_penalty = accounting._process_native_transfers(indexed_slippage) - expected_total_penalty = -amount_of_transfer - self.assertEqual(total_penalty, expected_total_penalty) - - def test_process_rewards(self): - cow_reward = 600 * ONE_ETH - mixed_transfers = [ - Transfer( - token=self.cow_token, recipient=self.solver, amount_wei=cow_reward - ), - ] - accounting = SplitTransfers(self.period, mixed_transfers, PrintStore()) - reward_target = Address.from_int(7) - redirect_map = { - self.solver: Vouch( - solver=self.solver, - reward_target=reward_target, - bonding_pool=Address.zero(), - ) - } - slippage_amount = 1 - positive_slippage = [ - SolverSlippage( - amount_wei=slippage_amount, solver_address=self.solver, solver_name="" - ) - ] - accounting._process_rewards(redirect_map, positive_slippage) - # Although we haven't called process_native_transfers, we are appending positive slippage inside - self.assertEqual( - accounting.eth_transfers, - [ - redirected_transfer( - token=None, - recipient=self.solver, - amount_wei=slippage_amount, - redirect=redirect_map[self.solver].reward_target, - ) - ], - ) - self.assertEqual( - accounting.cow_transfers, - [ - redirected_transfer( - token=self.cow_token, - recipient=self.solver, - amount_wei=cow_reward, - redirect=redirect_map[self.solver].reward_target, - ) - ], - ) - - def test_full_process_with_positive_slippage(self): - eth_amount = 2 * ONE_ETH - cow_reward = 600 * ONE_ETH - slippage_amount = 1 * ONE_ETH - accounting = self.construct_split_transfers_and_process( - solvers=[self.solver], - eth_amounts=[eth_amount], - cow_rewards=[cow_reward], - slippage_amounts=[slippage_amount], - redirects=self.redirect_map, - ) - - self.assertEqual( - accounting.eth_transfers, - [ - # The ETH Spent - Transfer( - token=None, - recipient=self.solver, - amount_wei=eth_amount, - ), - # The redirected positive slippage - redirected_transfer( - token=None, - recipient=self.solver, - amount_wei=slippage_amount, - redirect=self.redirect_map[self.solver].reward_target, - ), - ], - ) - self.assertEqual( - accounting.cow_transfers, - [ - redirected_transfer( - token=self.cow_token, - recipient=self.solver, - amount_wei=cow_reward, - redirect=self.redirect_map[self.solver].reward_target, - ), - ], - ) - self.assertEqual(accounting.unprocessed_cow, []) - self.assertEqual(accounting.unprocessed_native, []) - - def test_process_with_negative_slippage_not_exceeding_eth(self): - eth_amount = 2 * ONE_ETH - cow_reward = 600 * ONE_ETH - slippage_amount = -1 * ONE_ETH - accounting = self.construct_split_transfers_and_process( - solvers=[self.solver], - eth_amounts=[eth_amount], - cow_rewards=[cow_reward], - slippage_amounts=[slippage_amount], - redirects=self.redirect_map, - ) - - self.assertEqual( - accounting.eth_transfers, - [ - Transfer( - token=None, - recipient=self.solver, - # Slippage is negative (so it is added here) - amount_wei=eth_amount + slippage_amount, - ), - ], - ) - self.assertEqual( - accounting.cow_transfers, - [ - redirected_transfer( - token=self.cow_token, - recipient=self.solver, - amount_wei=cow_reward, - redirect=self.redirect_map[self.solver].reward_target, - ), - ], - ) - self.assertEqual(accounting.unprocessed_cow, []) - self.assertEqual(accounting.unprocessed_native, []) - - def test_process_with_overdraft_exceeding_eth_not_cow(self): - eth_amount = 2 * ONE_ETH - cow_reward = 100_000 * ONE_ETH # This is huge so COW is not exceeded! - slippage_amount = -3 * ONE_ETH - accounting = self.construct_split_transfers_and_process( - solvers=[self.solver], - eth_amounts=[eth_amount], - cow_rewards=[cow_reward], - slippage_amounts=[slippage_amount], - redirects=self.redirect_map, - ) - self.assertEqual( - accounting.eth_transfers, - [], - "No ETH reimbursement! when slippage exceeds eth_spent", - ) - self.assertEqual( - accounting.cow_transfers, - [ - redirected_transfer( - token=self.cow_token, - recipient=self.solver, - # This is the amount of COW deducted based on a "deterministic" price - # on the date of the fixed accounting period. - amount_wei=cow_reward - 25369802491025623613440, - redirect=self.redirect_map[self.solver].reward_target, - ) - ], - ) - self.assertEqual(accounting.unprocessed_cow, []) - self.assertEqual(accounting.unprocessed_native, []) - - def test_process_with_overdraft_exceeding_both_eth_and_cow(self): - eth_amount = 1 * ONE_ETH - cow_reward = 1000 * ONE_ETH - slippage_amount = -3 * ONE_ETH - - accounting = self.construct_split_transfers_and_process( - solvers=[self.solver], - eth_amounts=[eth_amount], - cow_rewards=[cow_reward], - slippage_amounts=[slippage_amount], - redirects=self.redirect_map, - ) - # Solver get no ETH reimbursement and no COW tokens - self.assertEqual(accounting.eth_transfers, []) - self.assertEqual(accounting.cow_transfers, []) - # Additional overdraft appended to overdrafts. - self.assertEqual( - accounting.overdrafts, - { - self.solver: Overdraft( - period=self.period, - account=self.solver, - name=self.solver_name, - wei=1960583059314169344, - ) - }, - ) - self.assertEqual(accounting.unprocessed_cow, []) - self.assertEqual(accounting.unprocessed_native, []) - - def test_process_with_missing_redirect(self): - """ - Solver has 1 ETH of positive slippage and COW reward but no redirect address is supplied. - The solver itself should receive 2 ETH transfers + 1 COW transfer. - Note that the 2 ETH transfers get "consolidated" (or squashed) in to - one only later in the process via `Transfer.consolidate` - """ - - eth_amount = 1 * ONE_ETH - cow_reward = 100 * ONE_ETH - slippage_amount = 1 * ONE_ETH - accounting = self.construct_split_transfers_and_process( - solvers=[self.solver], - eth_amounts=[eth_amount], - cow_rewards=[cow_reward], - slippage_amounts=[slippage_amount], - redirects={}, # Note the empty Redirect mapping! - ) - - self.assertEqual( - accounting.eth_transfers, - [ - Transfer( - token=None, - recipient=self.solver, - amount_wei=eth_amount, - ), - Transfer( - token=None, - recipient=self.solver, - amount_wei=slippage_amount, - ), - ], - ) - self.assertEqual( - accounting.cow_transfers, - [ - Transfer( - token=self.cow_token, - recipient=self.solver, - amount_wei=cow_reward, - ), - ], - ) - self.assertEqual(accounting.unprocessed_cow, []) - self.assertEqual(accounting.unprocessed_native, []) - - # Just for info, but not relevant to this test - demonstrating that these transfers are squashed. - consolidated_transfers = Transfer.consolidate(accounting.eth_transfers) - self.assertEqual(len(consolidated_transfers), 1) - self.assertEqual( - consolidated_transfers[0].amount_wei, eth_amount + slippage_amount - ) - - def test_process_multiple_solver_same_reward_target(self): - """ - Two solvers having their eth reimbursement sent to themselves, - but COW rewards going to the same target. - """ - solvers = [Address.from_int(1), Address.from_int(2)] - reward_target = Address.from_int(3) - eth_amounts = [1 * ONE_ETH, 2 * ONE_ETH] - cow_rewards = [100 * ONE_ETH, 200 * ONE_ETH] - accounting = self.construct_split_transfers_and_process( - solvers, - eth_amounts, - cow_rewards, - slippage_amounts=[], - redirects={ - solvers[0]: Vouch( - solver=solvers[0], - reward_target=reward_target, - bonding_pool=Address.zero(), - ), - solvers[1]: Vouch( - solver=solvers[1], - reward_target=reward_target, - bonding_pool=Address.zero(), - ), - }, - ) - - self.assertEqual( - accounting.eth_transfers, - [ - Transfer( - token=None, - recipient=solvers[0], - amount_wei=eth_amounts[0], - ), - Transfer( - token=None, - recipient=solvers[1], - amount_wei=eth_amounts[1], - ), - ], - ) - self.assertEqual( - accounting.cow_transfers, - [ - redirected_transfer( - token=self.cow_token, - recipient=solvers[0], - amount_wei=cow_rewards[0], - redirect=reward_target, - ), - redirected_transfer( - token=self.cow_token, - recipient=solvers[1], - amount_wei=cow_rewards[1], - redirect=reward_target, - ), - ], - ) - self.assertEqual(accounting.unprocessed_cow, []) - self.assertEqual(accounting.unprocessed_native, []) - - -if __name__ == "__main__": - unittest.main()