diff --git a/helpers/sett/SnapshotManager.py b/helpers/sett/SnapshotManager.py index 778a1ca4..9a498187 100644 --- a/helpers/sett/SnapshotManager.py +++ b/helpers/sett/SnapshotManager.py @@ -2,6 +2,8 @@ Controller, interface, chain, + Transaction, + Contract ) from tabulate import tabulate from rich.console import Console @@ -20,8 +22,15 @@ StrategySushiDiggWbtcLpOptimizerResolver, StrategyDiggLpMetaFarmResolver, ) -from helpers.utils import digg_shares_to_initial_fragments, val +from helpers.utils import ( + digg_shares_to_initial_fragments, + val, + send_transaction_to_discord, +) from scripts.systems.badger_system import BadgerSystem +from datetime import datetime +import time +from decimal import Decimal console = Console() @@ -172,7 +181,7 @@ def init_resolver(self, name): if name == "StrategyPancakeLpOptimizer": return StrategyBasePancakeResolver(self) - def settTend(self, overrides, confirm=True): + def settTend(self, overrides: dict, confirm: bool = True) -> Transaction: user = overrides["from"].address trackedUsers = {"user": user} before = self.snap(trackedUsers) @@ -180,8 +189,14 @@ def settTend(self, overrides, confirm=True): after = self.snap(trackedUsers) if confirm: self.resolver.confirm_tend(before, after, tx) - - def settTendViaManager(self, strategy, overrides, confirm=True): + return tx + + def settTendViaManager( + self, + strategy: Contract, + overrides: dict, + confirm: bool = True + ) -> Transaction: user = overrides["from"].address trackedUsers = {"user": user} before = self.snap(trackedUsers) @@ -189,8 +204,33 @@ def settTendViaManager(self, strategy, overrides, confirm=True): after = self.snap(trackedUsers) if confirm: self.resolver.confirm_tend(before, after, tx) - - def settHarvestViaManager(self, strategy, overrides, confirm=True): + return tx + + def settTendAndProcessTx( + self, + overrides: dict, + confirm: bool = True, + tended: Decimal = None + ): + tx = self.settTend(overrides, confirm) + self.confirmTransaction(tx, tended) + + def settTendViaManagerAndProcessTx( + self, + strategy: Contract, + overrides: dict, + confirm: bool = True, + tended: Decimal = None + ): + tx = self.settTendViaManager(strategy, overrides, confirm) + self.confirmTransaction(tx, tended) + + def settHarvestViaManager( + self, + strategy: Contract, + overrides: dict, + confirm: bool = True + ) -> Transaction: user = overrides["from"].address trackedUsers = {"user": user} before = self.snap(trackedUsers) @@ -198,8 +238,9 @@ def settHarvestViaManager(self, strategy, overrides, confirm=True): after = self.snap(trackedUsers) if confirm: self.resolver.confirm_harvest(before, after, tx) + return tx - def settHarvest(self, overrides, confirm=True): + def settHarvest(self, overrides: dict, confirm: bool = True) -> Transaction: user = overrides["from"].address trackedUsers = {"user": user} before = self.snap(trackedUsers) @@ -207,6 +248,39 @@ def settHarvest(self, overrides, confirm=True): after = self.snap(trackedUsers) if confirm: self.resolver.confirm_harvest(before, after, tx) + return tx + + def settHarvestViaManagerAndProcessTx( + self, + strategy: Contract, + overrides: dict, + confirm: bool = True, + harvested: Decimal = None + ): + tx = self.settHarvestViaManager(strategy, overrides, confirm) + self.confirmTransaction(tx, harvested) + + def settHarvestAndProcessTx( + self, + overrides: dict, + confirm: bool = True, + harvested: Decimal = None + ): + tx = self.settHarvest(overrides, confirm) + self.confirmTransaction(tx, harvested) + + def confirmTransaction(self, tx: Transaction, amount: Decimal): + success = True + if tx.error() == None and tx.revert_msg == None: + console.print(f"Transaction succeded!") + else: + # something went wrong + console.print(f"ERROR: harvest errored or reverted.") + console.print(f"Error: {tx.error()}") + console.print(f"Revert: {tx.revert_msg}") + success = False + + send_transaction_to_discord(tx, self.strategy.getName(), amount=amount, success=success) def settDeposit(self, amount, overrides, confirm=True): user = overrides["from"].address diff --git a/helpers/utils.py b/helpers/utils.py index a6d7c026..f98c742f 100644 --- a/helpers/utils.py +++ b/helpers/utils.py @@ -1,8 +1,14 @@ import time from brownie import * +from decimal import Decimal +from dotenv import load_dotenv from rich.console import Console from tabulate import tabulate +import requests +from discord import Webhook, RequestsWebhookAdapter, Embed +import os console = Console() +load_dotenv() # Assert approximate integer def approx(actual, expected, percentage_threshold): @@ -131,3 +137,36 @@ def snapSharesMatchForToken(snap, otherSnap, tokenKey): if shares != otherShares: return False return True + +def send_transaction_to_discord( + tx: Transaction, + strategy_name: str, + amount: Decimal = None, + success: bool = True +): + webhook = Webhook.from_url(os.getenv("DISCORD_WEBHOOK_URL"), adapter=RequestsWebhookAdapter()) + etherscan_url = f"https://etherscan.io/tx/{tx.txid}" + if success: + embed = Embed( + title="**Keeper transaction SUCCESS**" + ) + else: + embed = Embed( + title="**Keeper transaction FAILED**" + ) + if "harvest" in tx.fn_name: + embed.add_field( + name="Keeper Action", value=f"Harvest ${str(round(amount))} for {strategy_name}.", inline=False + ) + embed.add_field( + name="Etherscan Transaction", value=f"{etherscan_url}", inline=False + ) + webhook.send(embed=embed, username="Sushi Harvester") + elif "tend" in tx.fn_name: + embed.add_field( + name="Keeper Action", value=f"Tend ${str(round(amount))} for {strategy_name}.", inline=False + ) + embed.add_field( + name="Etherscan Transaction", value=f"{etherscan_url}", inline=False + ) + webhook.send(embed=embed, username="Sushi Tender") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index fd0c3ab3..54514354 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ colorama==0.4.3 commonmark==0.9.1 contextlib2==0.6.0 cytoolz==0.11.0 +discord.py==1.7.1 distlib==0.3.0 distro==1.4.0 dotmap==1.3.23 diff --git a/scripts/keeper/harvest_crv.py b/scripts/keeper/harvest_crv.py new file mode 100644 index 00000000..f72122ea --- /dev/null +++ b/scripts/keeper/harvest_crv.py @@ -0,0 +1,157 @@ +from helpers.sett.SnapshotManager import SnapshotManager +from config.keeper import keeper_config +from helpers.utils import tx_wait, val +from brownie import * +from helpers.gas_utils import gas_strategies +from brownie.network.gas.strategies import ( + GasNowStrategy, + ExponentialScalingStrategy, + SimpleGasStrategy, +) +from rich.console import Console +from scripts.systems.badger_system import BadgerSystem, connect_badger +from tabulate import tabulate +from decimal import Decimal + +gas_strategies.set_default_for_active_chain() + +console = Console() + +CRV_USD_CHAINLINK = "0xCd627aA160A6fA45Eb793D19Ef54f5062F20f33f" +CRV_ETH_CHAINLINK = "0x8a12Be339B0cD1829b91Adc01977caa5E9ac121e" +REN_CRV_STRATEGY = "0x444B860128B7Bf8C0e864bDc3b7a36a940db7D88" +SBTC_CRV_STRATEGY = "0x3Efc97A8e23f463e71Bf28Eb19690d097797eb17" +TBTC_CRV_STRATEGY = "0xE2fA197eAA5C726426003074147a08beaA59403B" + +XSUSHI_TOKEN = "0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272" + + +def harvest_ren_crv(badger: BadgerSystem = None): + harvest( + badger=badger, + strategy_address=REN_CRV_STRATEGY, + rewards_price_feed_address=CRV_ETH_CHAINLINK, + key="native.renCrv", + ) + + +def harvest_sbtc_crv(badger: BadgerSystem = None): + harvest( + badger=badger, + strategy_address=SBTC_CRV_STRATEGY, + rewards_price_feed_address=CRV_ETH_CHAINLINK, + key="native.sbtcCrv", + ) + + +def harvest_tbtc_crv(badger: BadgerSystem = None): + harvest( + badger=badger, + strategy_address=TBTC_CRV_STRATEGY, + rewards_price_feed_address=CRV_ETH_CHAINLINK, + key="native.tbtcCrv", + ) + + +def harvest( + badger: BadgerSystem = None, + strategy_address: str = None, + rewards_price_feed_address: str = None, + key: str = None, +): + strategy = Contract.from_explorer(strategy_address) + keeper_address = strategy.keeper.call() + + crv = Contract.from_explorer(strategy.crv.call()) + gauge = Contract.from_explorer(strategy.gauge()) + + decimals = 18 + + claimable_rewards = get_harvestable_amount( + decimals=crv.decimals(), + strategy=strategy, + gauge=gauge, + ) + console.print(f"claimable rewards: {claimable_rewards}") + + current_price_eth = get_current_price( + price_feed_address=rewards_price_feed_address, + decimals=crv.decimals() + ) + console.print(f"current rewards price per token (ETH): {current_price_eth}") + + gas_fee = estimate_gas_fee(strategy, keeper_address) + console.print(f"estimated gas fee to harvest: {gas_fee}") + + should_harvest = is_profitable(claimable_rewards, current_price_eth, gas_fee) + console.print(f"Should we harvest: {should_harvest}") + + if should_harvest and badger: + # we should actually take the snapshot and claim here + snap = SnapshotManager(badger, key) + before = snap.snap() + keeper = accounts.at(keeper_address) + crv_usd_oracle = Contract.from_explorer(CRV_USD_CHAINLINK) + crv_usd_price = Decimal(crv_usd_oracle.latestRoundData.call()[1] / 10 ** 8) + + if strategy.keeper() == badger.badgerRewardsManager: + snap.settHarvestViaManagerAndProcessTx( + strategy=strategy, + overrides={"from": keeper, "gas_limit": 2000000, "allow_revert": True}, + confirm=False, + harvested=claimable_rewards * crv_usd_price, + ) + else: + snap.settHarvestAndProcessTx( + overrides={"from": keeper, "gas_limit": 2000000, "allow_revert": True}, + confirm=False, + harvested=claimable_rewards * crv_usd_price, + ) + + +def get_harvestable_amount( + decimals: int, + strategy: Contract = None, + gauge: Contract = None +) -> Decimal: + harvestable_amt = gauge.claimable_tokens.call(strategy.address) / 10 ** decimals + return Decimal(harvestable_amt) + + +def get_current_price( + price_feed_address: str, + decimals: int, +) -> Decimal: + price_feed = Contract.from_explorer(price_feed_address) + return Decimal(price_feed.latestRoundData.call()[1] / 10 ** decimals) + + +def estimate_gas_fee(strategy: Contract, keeper: str) -> Decimal: + estimated_gas_to_harvest = strategy.harvest.estimate_gas({"from": keeper}) + current_gas_price = GasNowStrategy("fast").get_gas_price() / 10 ** 18 + return Decimal(current_gas_price * estimated_gas_to_harvest) + + +def is_profitable(amount: Decimal, price_per: Decimal, gas_fee: Decimal) -> bool: + fee_percent_of_claim = ( + 1 if amount * price_per == 0 else gas_fee / (amount * price_per) + ) + console.print(f"Fee as percent of harvest: {round(fee_percent_of_claim * 100, 2)}%") + return fee_percent_of_claim <= 0.05 + + +def main(): + badger = connect_badger(load_keeper=True) + # skip = keeper_config.get_active_chain_skipped_setts("harvest") + # console.print(badger.getAllSettIds()) + + # harvest_all(badger, skip) + + console.print("harvesting ren crv") + harvest_ren_crv(badger=badger) + console.print("-------------------") + console.print("harvesting sbtc crv") + harvest_sbtc_crv(badger=badger) + console.print("-------------------") + console.print("harvesting tbtc crv") + harvest_tbtc_crv(badger=badger) diff --git a/scripts/keeper/harvest_sushi.py b/scripts/keeper/harvest_sushi.py new file mode 100644 index 00000000..ec05adb2 --- /dev/null +++ b/scripts/keeper/harvest_sushi.py @@ -0,0 +1,165 @@ +from helpers.sett.SnapshotManager import SnapshotManager +from config.keeper import keeper_config +from helpers.utils import tx_wait, val +from brownie import * +from helpers.gas_utils import gas_strategies +from brownie.network.gas.strategies import ( + GasNowStrategy, + ExponentialScalingStrategy, + SimpleGasStrategy, +) +from rich.console import Console +from scripts.systems.badger_system import BadgerSystem, connect_badger +from tabulate import tabulate +from decimal import Decimal + +gas_strategies.set_default_for_active_chain() + +console = Console() + +ETH_USD_CHAINLINK = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419" +SUSHI_ETH_CHAINLINK = "0xe572CeF69f43c2E488b33924AF04BDacE19079cf" +WBTC_ETH_STRATEGY = "0x7A56d65254705B4Def63c68488C0182968C452ce" +WBTC_DIGG_STRATEGY = "0xaa8dddfe7DFA3C3269f1910d89E4413dD006D08a" +WBTC_BADGER_STRATEGY = "0x3a494D79AA78118795daad8AeFF5825C6c8dF7F1" + +XSUSHI_TOKEN = "0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272" + + +def harvest_wbtc_eth(badger: BadgerSystem = None): + harvest_sushi( + badger=badger, + strategy_address=WBTC_ETH_STRATEGY, + rewards_price_feed_address=SUSHI_ETH_CHAINLINK, + key="native.sushiWbtcEth", + ) + + +def harvest_wbtc_digg(badger: BadgerSystem = None): + harvest_sushi( + badger=badger, + strategy_address=WBTC_DIGG_STRATEGY, + rewards_price_feed_address=SUSHI_ETH_CHAINLINK, + key="native.sushiDiggWbtc", + ) + + +def harvest_wbtc_badger(badger: BadgerSystem = None): + harvest_sushi( + badger=badger, + strategy_address=WBTC_BADGER_STRATEGY, + rewards_price_feed_address=SUSHI_ETH_CHAINLINK, + key="native.sushiBadgerWbtc", + ) + + +def harvest_sushi( + badger: BadgerSystem = None, + strategy_address: str = None, + rewards_price_feed_address: str = None, + key: str = None, +): + strategy = Contract.from_explorer(strategy_address) + keeper_address = strategy.keeper.call() + pool_id = strategy.pid.call() + + xsushi = Contract.from_explorer(XSUSHI_TOKEN) + sushi = Contract.from_explorer(xsushi.sushi.call()) + + decimals = 18 + + claimable_rewards = get_harvestable_xsushi( + decimals=decimals, + pool_id=pool_id, + strategy_address=strategy_address, + xsushi=xsushi, + ) + console.print(f"claimable rewards: {claimable_rewards}") + + current_price_eth = get_current_price( + price_feed_address=rewards_price_feed_address, + decimals=decimals, + xsushi=xsushi, + sushi=sushi, + ) + console.print(f"current rewards price per token (ETH): {current_price_eth}") + + gas_fee = estimate_gas_fee(strategy, keeper_address) + console.print(f"estimated gas fee to harvest: {gas_fee}") + + should_harvest = is_profitable(claimable_rewards, current_price_eth, gas_fee) + console.print(f"Should we harvest: {should_harvest}") + + if should_harvest and badger: + # we should actually take the snapshot and claim here + snap = SnapshotManager(badger, key) + before = snap.snap() + keeper = accounts.at(keeper_address) + eth_usd_oracle = Contract.from_explorer(ETH_USD_CHAINLINK) + eth_usd_price = Decimal(eth_usd_oracle.latestRoundData.call()[1] / 10 ** 8) + + if strategy.keeper() == badger.badgerRewardsManager: + snap.settHarvestViaManagerAndProcessTx( + strategy=strategy, + overrides={"from": keeper, "gas_limit": 2000000, "allow_revert": True}, + confirm=False, + harvested=claimable_rewards * current_price_eth * eth_usd_price, + ) + else: + snap.settHarvestAndProcessTx( + overrides={"from": keeper, "gas_limit": 2000000, "allow_revert": True}, + confirm=False, + harvested=claimable_rewards * current_price_eth * eth_usd_price, + ) + + +def get_harvestable_xsushi( + decimals: int, + pool_id: int = None, + strategy_address: str = None, + xsushi: Contract = None, +) -> Decimal: + harvestable_amt = xsushi.balanceOf.call(strategy_address) / 10 ** decimals + return Decimal(harvestable_amt) + + +def get_current_price( + price_feed_address: str, + decimals: int, + xsushi: Contract = None, + sushi: Contract = None, +) -> Decimal: + price_feed = Contract.from_explorer(price_feed_address) + ratio = sushi.balanceOf.call(xsushi.address) / xsushi.totalSupply.call() + return Decimal((price_feed.latestRoundData.call()[1] / 10 ** decimals) * ratio) + + +def estimate_gas_fee(strategy: Contract, keeper: str) -> Decimal: + estimated_gas_to_harvest = strategy.harvest.estimate_gas({"from": keeper}) + current_gas_price = GasNowStrategy("fast").get_gas_price() / 10 ** 18 + return Decimal(current_gas_price * estimated_gas_to_harvest) + + +def is_profitable(amount: Decimal, price_per: Decimal, gas_fee: Decimal) -> bool: + fee_percent_of_claim = ( + 1 if amount * price_per == 0 else gas_fee / (amount * price_per) + ) + console.print(f"Fee as percent of harvest: {round(fee_percent_of_claim * 100, 2)}%") + return fee_percent_of_claim <= 0.05 + + +def main(): + badger = connect_badger(load_keeper=True) + # skip = keeper_config.get_active_chain_skipped_setts("harvest") + # console.print(badger.getAllSettIds()) + + # harvest_all(badger, skip) + + console.print("harvesting wbtc eth") + harvest_wbtc_eth(badger=badger) + console.print("-------------------") + console.print("harvesting wbtc digg") + harvest_wbtc_digg(badger=badger) + console.print("-------------------") + console.print("harvesting wbtc badger") + harvest_wbtc_badger(badger=badger) diff --git a/scripts/keeper/tend_sushi.py b/scripts/keeper/tend_sushi.py new file mode 100644 index 00000000..ee7d1945 --- /dev/null +++ b/scripts/keeper/tend_sushi.py @@ -0,0 +1,145 @@ +from helpers.sett.SnapshotManager import SnapshotManager +from config.keeper import keeper_config +from helpers.utils import tx_wait, val +from brownie import * +from helpers.gas_utils import gas_strategies +from brownie.network.gas.strategies import ( + GasNowStrategy, + ExponentialScalingStrategy, + SimpleGasStrategy, +) +from rich.console import Console +from scripts.systems.badger_system import BadgerSystem, connect_badger +from tabulate import tabulate +from decimal import Decimal + +gas_strategies.set_default_for_active_chain() + +console = Console() + +ETH_USD_CHAINLINK = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419" +SUSHI_ETH_CHAINLINK = "0xe572CeF69f43c2E488b33924AF04BDacE19079cf" +WBTC_ETH_STRATEGY = "0x7A56d65254705B4Def63c68488C0182968C452ce" +WBTC_DIGG_STRATEGY = "0xaa8dddfe7DFA3C3269f1910d89E4413dD006D08a" +WBTC_BADGER_STRATEGY = "0x3a494D79AA78118795daad8AeFF5825C6c8dF7F1" + +def tend_wbtc_eth(badger: BadgerSystem): + tend_sushi( + badger=badger, + strategy_address=WBTC_ETH_STRATEGY, + rewards_price_feed_address=SUSHI_ETH_CHAINLINK, + key="native.sushiWbtcEth", + ) + + +def tend_wbtc_digg(badger: BadgerSystem): + tend_sushi( + badger=badger, + strategy_address=WBTC_DIGG_STRATEGY, + rewards_price_feed_address=SUSHI_ETH_CHAINLINK, + key="native.sushiDiggWbtc", + ) + + +def tend_wbtc_badger(badger: BadgerSystem): + tend_sushi( + badger=badger, + strategy_address=WBTC_BADGER_STRATEGY, + rewards_price_feed_address=SUSHI_ETH_CHAINLINK, + key="native.sushiBadgerWbtc", + ) + + +def tend_sushi( + badger: BadgerSystem = None, + strategy_address: str = None, + rewards_price_feed_address: str = None, + key: str = None +): + + strategy = Contract.from_explorer(strategy_address) + keeper_address = strategy.keeper() + pool_id = strategy.pid.call() + + decimals = 18 + + claimable_rewards = get_claimable_sushi( + decimals=decimals, pool_id=pool_id, strategy_address=strategy_address + ) + console.print(f"claimable rewards: {claimable_rewards}") + + current_price_eth = get_current_price( + price_feed_address=rewards_price_feed_address, decimals=decimals + ) + console.print(f"current rewards price per token (ETH): {current_price_eth}") + + gas_fee = estimate_gas_fee(strategy, keeper_address) + console.print(f"estimated gas fee to tend: {gas_fee}") + + should_tend = is_profitable(claimable_rewards, current_price_eth, gas_fee) + console.print(f"Should we tend: {should_tend}") + + if should_tend: + # we should actually take the snapshot and claim here + snap = SnapshotManager(badger, key) + before = snap.snap() + keeper = accounts.at(keeper_address) + eth_usd_oracle = Contract.from_explorer(ETH_USD_CHAINLINK) + eth_usd_price = Decimal(eth_usd_oracle.latestRoundData.call()[1] / 10 ** 8) + + if strategy.keeper() == badger.badgerRewardsManager: + snap.settTendViaManagerAndProcessTx( + strategy=strategy, + overrides={"from": keeper, "gas_limit": 2000000, "allow_revert": True}, + confirm=False, + tended=claimable_rewards * current_price_eth * eth_usd_price, + ) + else: + snap.settTendAndProcessTx( + overrides={"from": keeper, "gas_limit": 2000000, "allow_revert": True}, + confirm=False, + tended=claimable_rewards * current_price_eth * eth_usd_price, + ) + + +def get_claimable_sushi( + decimals: int, pool_id: int = None, strategy_address: str = None +) -> Decimal: + chef = Contract.from_explorer("0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd") + claimable_rewards = ( + chef.pendingSushi.call(pool_id, strategy_address) / 10 ** decimals + ) + return Decimal(claimable_rewards) + + +def get_current_price(price_feed_address: str, decimals: int) -> Decimal: + price_feed = Contract.from_explorer(price_feed_address) + return Decimal(price_feed.latestRoundData.call()[1] / 10 ** decimals) + + +def estimate_gas_fee(strategy: Contract, keeper: str) -> Decimal: + estimated_gas_to_tend = strategy.tend.estimate_gas({"from": keeper}) + current_gas_price = GasNowStrategy("fast").get_gas_price() / 10 ** 18 + return Decimal(current_gas_price * estimated_gas_to_tend) + + +def is_profitable(amount: Decimal, price_per: Decimal, gas_fee: Decimal) -> bool: + fee_percent_of_claim = gas_fee / (amount * price_per) + console.print(f"Fee as percent of claim: {round(fee_percent_of_claim * 100, 2)}%") + return (gas_fee / (amount * price_per)) <= 0.01 + + +def main(): + badger = connect_badger(load_keeper=True) + # skip = keeper_config.get_active_chain_skipped_setts("tend") + # console.print(badger.getAllSettIds()) + + # tend_all(badger, skip) + console.print("tending wbtc eth") + tend_wbtc_eth(badger) + console.print("-------------------") + console.print("tending wbtc digg") + tend_wbtc_digg(badger) + console.print("-------------------") + console.print("tending wbtc badger") + tend_wbtc_badger(badger)