Skip to content

Commit

Permalink
CIP17: Symmetric Slippage (#163)
Browse files Browse the repository at this point in the history
Introducing an accounting of positive solver slippage. In particular, instead of ignoring the positive slippage, it is considered as part of the reward and redirected to the reward target from the Vouch Registry (i.e. the account receiving COW token rewards).
  • Loading branch information
bh2smith authored Jan 10, 2023
1 parent c7e9c85 commit 9908465
Show file tree
Hide file tree
Showing 7 changed files with 528 additions and 66 deletions.
12 changes: 6 additions & 6 deletions src/fetch/dune.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from src.models.vouch import Vouch, RECOGNIZED_BONDING_POOLS, parse_vouches
from src.pg_client import DualEnvDataframe
from src.queries import QUERIES, DuneVersion, QueryData
from src.utils.dataset import index_by
from src.utils.print_store import PrintStore

log = set_log(__name__)
Expand Down Expand Up @@ -198,11 +197,12 @@ def get_transfers(self) -> list[Transfer]:
# TODO - fetch these three results asynchronously!
reimbursements = self.get_eth_spent()
rewards = self.get_cow_rewards()
split_transfers = SplitTransfers(self.period, reimbursements + rewards)
negative_slippage = self.get_period_slippage().negative

split_transfers = SplitTransfers(
period=self.period,
mixed_transfers=reimbursements + rewards,
log_saver=self.log_saver,
)
return split_transfers.process(
indexed_slippage=index_by(negative_slippage, "solver_address"),
slippages=self.get_period_slippage(),
cow_redirects=self.get_vouches(),
log_saver=self.log_saver,
)
20 changes: 11 additions & 9 deletions src/models/slippage.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ def from_dict(cls, obj: dict[str, str]) -> SolverSlippage:
class SplitSlippages:
"""Basic class to store the output of slippage fetching"""

negative: list[SolverSlippage]
positive: list[SolverSlippage]
solvers_with_negative_total: list[SolverSlippage]
solvers_with_positive_total: list[SolverSlippage]

def __init__(self) -> None:
self.negative = []
self.positive = []
self.solvers_with_negative_total = []
self.solvers_with_positive_total = []

@classmethod
def from_data_set(cls, data_set: list[dict[str, str]]) -> SplitSlippages:
Expand All @@ -57,20 +57,22 @@ def from_data_set(cls, data_set: list[dict[str, str]]) -> SplitSlippages:
def append(self, slippage: SolverSlippage) -> None:
"""Appends the Slippage to the appropriate half based on signature of amount"""
if slippage.amount_wei < 0:
self.negative.append(slippage)
self.solvers_with_negative_total.append(slippage)
else:
self.positive.append(slippage)
self.solvers_with_positive_total.append(slippage)

def __len__(self) -> int:
return len(self.negative) + len(self.positive)
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.negative)
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.positive)
return sum(pos.amount_wei for pos in self.solvers_with_positive_total)


class QueryType(Enum):
Expand Down
79 changes: 53 additions & 26 deletions src/models/split_transfers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
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
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


Expand All @@ -27,7 +28,13 @@ class SplitTransfers:
Technically we should have two additional classes one for each token type.
"""

def __init__(self, period: AccountingPeriod, mixed_transfers: list[Transfer]):
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 = []
Expand All @@ -44,20 +51,27 @@ def __init__(self, period: AccountingPeriod, mixed_transfers: list[Transfer]):
self.cow_transfers: list[Transfer] = []

def _process_native_transfers(
self, indexed_slippage: dict[Address, SolverSlippage], log_saver: PrintStore
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.
"""
penalty_total = 0
while self.unprocessed_native:
transfer = self.unprocessed_native.pop(0)
solver = transfer.receiver
slippage: Optional[SolverSlippage] = indexed_slippage.get(solver)
if slippage is not None:
assert (
slippage.amount_wei < 0
), f"Expected negative slippage! Got {slippage}"
try:
transfer.add_slippage(slippage, log_saver)
transfer.add_slippage(slippage, self.log_saver)
penalty_total += slippage.amount_wei
except ValueError as err:
name, address = slippage.solver_name, slippage.solver_address
log_saver.print(
self.log_saver.print(
f"Slippage for {address}({name}) exceeds reimbursement: {err}\n"
f"Excluding payout and appending excess to overdraft",
category=Category.OVERDRAFT,
Expand All @@ -71,8 +85,10 @@ def _process_native_transfers(
self.eth_transfers.append(transfer)
return penalty_total

def _process_token_transfers(
self, cow_redirects: dict[Address, Vouch], log_saver: PrintStore
def _process_rewards(
self,
redirect_map: dict[Address, Vouch],
positive_slippage: list[SolverSlippage],
) -> None:
price_day = self.period.end - timedelta(days=1)
while self.unprocessed_cow:
Expand All @@ -82,13 +98,13 @@ def _process_token_transfers(
overdraft = self.overdrafts.pop(solver, None)
if overdraft is not None:
cow_deduction = eth_in_token(TokenId.COW, overdraft.wei, price_day)
log_saver.print(
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:
log_saver.print(
self.log_saver.print(
"Overdraft exceeds COW reward! "
"Excluding reward and updating overdraft",
category=Category.OVERDRAFT,
Expand All @@ -99,39 +115,50 @@ def _process_token_transfers(
# Reinsert since there is still an amount owed.
self.overdrafts[solver] = overdraft
continue
if solver in cow_redirects:
# Redirect COW rewards to reward target specific by VouchRegistry
redirect_address = cow_redirects[solver].reward_target
log_saver.print(
f"Redirecting solver {solver} COW tokens "
f"({transfer.amount}) to {redirect_address}",
category=Category.REDIRECT,
)
transfer.receiver = redirect_address
transfer.redirect_to(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.
while positive_slippage:
slippage = positive_slippage.pop()
assert (
slippage.amount_wei > 0
), f"Expected positive slippage got {slippage.amount_wei}"
slippage_transfer = Transfer.from_slippage(slippage)
slippage_transfer.redirect_to(redirect_map, self.log_saver)
self.eth_transfers.append(slippage_transfer)

def process(
self,
indexed_slippage: dict[Address, SolverSlippage],
slippages: SplitSlippages,
cow_redirects: dict[Address, Vouch],
log_saver: PrintStore,
) -> list[Transfer]:
"""
This is the public interface to construct the final transfer file based on
raw (unpenalized) results, slippage penalty, redirected rewards and overdrafts.
raw (unpenalized) results, positive, negative slippage, rewards and overdrafts.
It is very important that the native token transfers are processed first,
so that and overdraft from slippage can be carried over and deducted from
so that any overdraft from slippage can be carried over and deducted from
the COW rewards.
"""
penalty_total = self._process_native_transfers(indexed_slippage, log_saver)
self._process_token_transfers(cow_redirects, log_saver)
log_saver.print(
penalty_total = self._process_native_transfers(
indexed_slippage=index_by(
slippages.solvers_with_negative_total, "solver_address"
)
)
# Note that positive and negative slippage is DISJOINT.
# So no overdraft computations will overlap with the positive slippage perturbations.
self._process_rewards(
cow_redirects,
positive_slippage=slippages.solvers_with_positive_total,
)
self.log_saver.print(
f"Total Slippage deducted (ETH): {penalty_total / 10**18}",
category=Category.TOTALS,
)
if self.overdrafts:
accounts_owing = "\n".join(map(str, self.overdrafts.values()))
log_saver.print(
self.log_saver.print(
f"Additional owed\n {accounts_owing}", category=Category.OVERDRAFT
)
return self.cow_transfers + self.eth_transfers
Expand Down
3 changes: 3 additions & 0 deletions src/models/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ def __init__(self, address: str | Address, decimals: Optional[int] = None):
decimals if decimals is not None else get_token_decimals(web3, address)
)

def __repr__(self) -> str:
return str(self.address)

def __eq__(self, other: object) -> bool:
if isinstance(other, Token):
return self.address == other.address and self.decimals == other.decimals
Expand Down
31 changes: 31 additions & 0 deletions src/models/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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


Expand Down Expand Up @@ -186,3 +187,33 @@ def __str__(self) -> str:
f"amount_wei={self.amount})"
)
raise ValueError(f"Invalid Token Type {self.token_type}")

def redirect_to(
self, redirects: dict[Address, Vouch], log_saver: PrintStore
) -> None:
"""
Redirects Transfers via Address => Vouch.reward_target
This function modifies self!
"""
recipient = self.receiver
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.token or 'ETH'}"
f"({self.amount}) to {redirect_address}",
category=Category.REDIRECT,
)
self.receiver = 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,
receiver=slippage.solver_address,
amount_wei=slippage.amount_wei,
)
59 changes: 59 additions & 0 deletions tests/queries/test_slippage_investigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,65 @@ def test_no_outrageous_slippage(self):
for obj in top_five_negative + top_five_positive:
assert abs(int(obj["eth_slippage_wei"])) < 1 * 10**18

def test_simple_positive_slippage(self):
"""
Transaction 0x2509839cda8a4fec0c3b0ae4e90537aef5afaaeeee88427161d1b08367aeb0d7
contains a single UniV3 interaction and resulted in 82 USDC positive slippage.
"""
period = AccountingPeriod("2022-12-16", 1)
query = QUERIES["PERIOD_SLIPPAGE"].with_params(
period.as_query_params()
+ [
# Default values (on the query definition) do not need to be provided!
QueryParameter.text_type(
"TxHash",
"0x2509839cda8a4fec0c3b0ae4e90537aef5afaaeeee88427161d1b08367aeb0d7",
),
QueryParameter.text_type("CTE_NAME", "results_per_tx"),
],
dune_version=DuneVersion.V2,
)
results = exec_or_get(self.dune, query, result_id="01GP3A0QV1BNWF55Z5N362RK8M")
tx_slippage = results.get_rows()[0]
self.assertEqual(tx_slippage["eth_slippage_wei"], 71151929005056890)
self.assertAlmostEqual(
tx_slippage["usd_value"], 83.37280645114296, delta=0.00001
)

def test_positive_slippage_evaluation(self):
"""
Transaction 0x5c4e410ce5d741f60e06a8621c6f12839ad39273f5abf78d4bbc1cd3b31de46c
Alerted on January 1, 2023.
https://dune.com/queries/1688044?MinAbsoluteSlippageTolerance=100&TxHash=0x&RelativeSlippageTolerance=1.0&SignificantSlippageValue=2000&StartTime=2023-01-01+00%3A00%3A00&EndTime=2023-01-02+00%3A00%3A00&EndTime_d83555=2023-01-02+00%3A00%3A00&MinAbsoluteSlippageTolerance_n26d66=100&RelativeSlippageTolerance_n26d66=1.0&SignificantSlippageValue_n26d66=2000&StartTime_d83555=2023-01-01+00%3A00%3A00&TxHash_t6c1ea=0x
"""
period = AccountingPeriod("2023-01-01", 1)
query = QUERIES["PERIOD_SLIPPAGE"].with_params(
period.as_query_params()
+ [
# Default values (on the query definition) do not need to be provided!
QueryParameter.text_type(
"TxHash",
"0x5c4e410ce5d741f60e06a8621c6f12839ad39273f5abf78d4bbc1cd3b31de46c",
),
# QueryParameter.text_type("SolverAddress", "0x97ec0a17432d71a3234ef7173c6b48a2c0940896"),
# QueryParameter.text_type("TokenList", ",".join(get_trusted_tokens())),
QueryParameter.text_type("CTE_NAME", "results_per_tx"),
],
dune_version=DuneVersion.V2,
)
results = exec_or_get(self.dune, query, result_id="01GP11D7FH4WAEFW1Z46Q79VBC")
tx_slippage = results.get_rows()[0]
self.assertEqual(tx_slippage["eth_slippage_wei"], 148427839329771300)
self.assertAlmostEqual(
tx_slippage["usd_value"], 177.37732880251593, delta=0.000000001
)
# When looking at the pure batch token imbalance:
# https://dune.com/queries/1380984?TxHash=0x5c4e410ce5d741f60e06a8621c6f12839ad39273f5abf78d4bbc1cd3b31de46c
# One sees 5 tokens listed. However, this calculation merges ETH/WETH together
# as a single row one of those imbalances was excluded as an "internal trade"
# https://dune.com/queries/1836718?CTE_NAME_e15077=final_token_balance_sheet&EndTime_d83555=2023-01-02+00%3A00%3A00&StartTime_d83555=2023-01-01+00%3A00%3A00&TxHash_t6c1ea=0x5c4e410ce5d741f60e06a8621c6f12839ad39273f5abf78d4bbc1cd3b31de46c
self.assertEqual(tx_slippage["num_entries"], 4)


if __name__ == "__main__":
unittest.main()
Loading

0 comments on commit 9908465

Please sign in to comment.