Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement osToken force exit and claim #5

Merged
merged 20 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ HOT_WALLET_PRIVATE_KEY=0x2......
# Settings for update_ltv.py
# EXECUTION_ENDPOINT=https://execution
# GRAPH_API_URL=https://graph

# Settings for force_exit.py
# EXECUTION_ENDPOINT=https://execution
# GRAPH_API_URL=https://graph
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# `python-base` sets up all our shared environment variables
FROM python:3.10.14-alpine3.20 as python-base
FROM python:3.10.16-alpine3.21 as python-base

# python
ENV PYTHONUNBUFFERED=1 \
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ List of scripts:

* `src/update_price.py` - calls the function on the mainnet that updates the osETH-to-ETH price on the L2 network, i.e., the target network.
* `src/update_ltv.py` - updates user having maximum LTV in given vault. Users are stored in `VaultUserLtvTracker` contract.
* `src/force_exit.py` - Monitor leverage positions and trigger exits/claims for those that approach the liquidation threshold.

## Setup

Expand Down
25 changes: 5 additions & 20 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
[tool.poetry]
name = "periodic-tasks"
version = "v0.1.1"
version = "v0.2.0"
description = ""
authors = ["StakeWise Labs <info@stakewise.io>"]
readme = "README.md"
package-mode = false

[tool.poetry.dependencies]
python = "^3.10"
gql = {extras = ["requests"], version = "==3.5.0"}
sw-utils = {git = "https://github.com/stakewise/sw-utils.git", rev = "v0.6.25"}
sw-utils = {git = "https://github.com/stakewise/sw-utils.git", rev = "v0.7.3"}
python-decouple = "==3.8"
aiohttp = "==3.10.11"
sentry-sdk = "==2.19.2"
Expand Down Expand Up @@ -84,3 +83,6 @@ exclude = '''

[tool.vulture]
exclude = ["networks.py"]
ignore_names = [
"default_account", # execution client
]
1 change: 1 addition & 0 deletions src/common/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def get_execution_client(endpoint: str, account: LocalAccount | None = None) ->
client = Web3(Web3.HTTPProvider(endpoint))
if account:
client.middleware_onion.add(construct_sign_and_send_raw_middleware(account))
client.eth.default_account = account.address
return client


Expand Down
15 changes: 14 additions & 1 deletion src/common/contracts.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import json
import logging

from eth_typing import ChecksumAddress
from eth_typing import ChecksumAddress, HexStr
from hexbytes import HexBytes
from web3 import Web3
from web3.contract.contract import ContractEvents, ContractFunctions
from web3.types import Wei

from .typings import HarvestParams

logger = logging.getLogger(__name__)

Expand All @@ -24,3 +28,12 @@ def functions(self) -> ContractFunctions:
@property
def events(self) -> ContractEvents:
return self.contract.events

def encode_abi(self, fn_name: str, args: list | None = None) -> HexStr:
return self.contract.encodeABI(fn_name=fn_name, args=args)

@staticmethod
def _get_zero_harvest_params() -> HarvestParams:
return HarvestParams(
rewards_root=HexBytes(b'\x00' * 32), reward=Wei(0), unlocked_mev_reward=Wei(0), proof=[]
)
48 changes: 48 additions & 0 deletions src/common/graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import logging

from eth_typing import ChecksumAddress
from gql import gql
from hexbytes import HexBytes
from sw_utils.graph.client import GraphClient
from web3 import Web3
from web3.types import Wei

from src.common.typings import HarvestParams

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


async def get_harvest_params(
graph_client: GraphClient, vault_address: ChecksumAddress
) -> HarvestParams | None:
query = gql(
"""
query VaultQuery($vault: String) {
vault(
id: $vault
) {
proof
proofReward
proofUnlockedMevReward
rewardsRoot
}
}
"""
)
params = {
'vault': vault_address.lower(),
}

response = await graph_client.run_query(query, params)
vault_data = response['vault'] # pylint: disable=unsubscriptable-object

if not all(vault_data):
return None

return HarvestParams(
rewards_root=HexBytes(Web3.to_bytes(hexstr=vault_data['rewardsRoot'])),
reward=Wei(int(vault_data['proofReward'])),
unlocked_mev_reward=Wei(int(vault_data['proofUnlockedMevReward'])),
proof=[HexBytes(Web3.to_bytes(hexstr=p)) for p in vault_data['proof']],
)
8 changes: 8 additions & 0 deletions src/common/logs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import logging

from gql.transport.aiohttp import log as requests_logger


def setup_gql_log_level() -> None:
"""Raise GQL default log level"""
requests_logger.setLevel(logging.WARNING)
136 changes: 78 additions & 58 deletions src/common/networks.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
from dataclasses import dataclass
from enum import Enum
from dataclasses import asdict, dataclass

from eth_typing import ChecksumAddress, HexAddress, HexStr
from ens.constants import EMPTY_ADDR_HEX
from eth_typing import ChecksumAddress
from sw_utils.networks import CHIADO, GNOSIS, HOLESKY, MAINNET
from sw_utils.networks import NETWORKS as BASE_NETWORKS
from sw_utils.networks import BaseNetworkConfig
from web3 import Web3

EMPTY_ADDR_HEX = HexAddress(HexStr('0x' + '00' * 20))
ZERO_CHECKSUM_ADDRESS = Web3.to_checksum_address(EMPTY_ADDR_HEX) # noqa


class Network(Enum):
MAINNET = 'mainnet'
HOLESKY = 'holesky'
GNOSIS = 'gnosis'
CHIADO = 'chiado'
SEPOLIA = 'sepolia'
SEPOLIA = 'sepolia'


@dataclass
Expand All @@ -29,72 +25,96 @@ class PriceNetworkConfig:
PRICE_FEED_SENDER_CONTRACT_ADDRESS: ChecksumAddress


PRICE_NETWORKS: dict[str, PriceNetworkConfig | None] = {
MAINNET: PriceNetworkConfig(
# TARGET_CHAIN is not what eth_chainId returns.
# It is internal id used in PriceFeedSender contract.
TARGET_CHAIN=23,
# PriceFeedReceiver contract address on Arbitrum
TARGET_ADDRESS=Web3.to_checksum_address('0xbd335c16c94be8c4dd073ae376ddf78bec1858df'),
# PriceFeed contract address on Arbitrum
TARGET_PRICE_FEED_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0xba74737a078c05500dd98c970909e4a3b90c35c6'
),
# PriceFeedSender contract address on Mainnet
PRICE_FEED_SENDER_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0xf7d4e7273e5015c96728a6b02f31c505ee184603'
),
),
HOLESKY: None,
GNOSIS: None,
CHIADO: None,
SEPOLIA: PriceNetworkConfig(
# TARGET_CHAIN is not what eth_chainId returns.
# It is internal id used in PriceFeedSender contract.
TARGET_CHAIN=10003,
# PriceFeedReceiver contract address on Arbitrum Sepolia
TARGET_ADDRESS=Web3.to_checksum_address('0x744836a91f5151c6ef730eb7e07c232997debaaa'),
# PriceFeed contract address on Arbitrum Sepolia
TARGET_PRICE_FEED_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0x4026affabd9032bcc87fa05c02f088905f3dc09b'
),
# PriceFeedSender contract address on Sepolia
PRICE_FEED_SENDER_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0xe572a8631a49ec4c334812bb692beecf934ac4e9'
),
),
}


@dataclass
class NetworkConfig:
class NetworkConfig(BaseNetworkConfig):
VAULT_USER_LTV_TRACKER_CONTRACT_ADDRESS: ChecksumAddress
PRICE_NETWORK_CONFIG: PriceNetworkConfig | None = None
LEVERAGE_STRATEGY_CONTRACT_ADDRESS: ChecksumAddress
STRATEGY_REGISTRY_CONTRACT_ADDRESS: ChecksumAddress
OSTOKEN_ESCROW_CONTRACT_ADDRESS: ChecksumAddress


NETWORKS: dict[Network, NetworkConfig] = {
Network.MAINNET: NetworkConfig(
NETWORKS: dict[str, NetworkConfig] = {
MAINNET: NetworkConfig(
**asdict(BASE_NETWORKS[MAINNET]),
VAULT_USER_LTV_TRACKER_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0xe0Ae8B04922d6e3fA06c2496A94EF2875EFcC7BB'
),
PRICE_NETWORK_CONFIG=(
PriceNetworkConfig(
# TARGET_CHAIN is not what eth_chainId returns.
# It is internal id used in PriceFeedSender contract.
TARGET_CHAIN=23,
# PriceFeedReceiver contract address on Arbitrum
TARGET_ADDRESS=Web3.to_checksum_address(
'0xbd335c16c94be8c4dd073ae376ddf78bec1858df'
),
# PriceFeed contract address on Arbitrum
TARGET_PRICE_FEED_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0xba74737a078c05500dd98c970909e4a3b90c35c6'
),
# PriceFeedSender contract address on Mainnet
PRICE_FEED_SENDER_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0xf7d4e7273e5015c96728a6b02f31c505ee184603'
),
)
LEVERAGE_STRATEGY_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0x48cD14FDB8e72A03C8D952af081DBB127D6281fc'
),
STRATEGY_REGISTRY_CONTRACT_ADDRESS=ZERO_CHECKSUM_ADDRESS,
OSTOKEN_ESCROW_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0x09e84205DF7c68907e619D07aFD90143c5763605'
),
),
Network.HOLESKY: NetworkConfig(
HOLESKY: NetworkConfig(
**asdict(BASE_NETWORKS[HOLESKY]),
VAULT_USER_LTV_TRACKER_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0x8f48130b9b96B58035b4A9389eCDaBC00d59d0c8'
),
LEVERAGE_STRATEGY_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0xdB38cfc6e98a34Cdc60c568f607417E646C75B34'
),
STRATEGY_REGISTRY_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0xFc8E3E7c919b4392D9F5B27015688e49c80015f0'
),
OSTOKEN_ESCROW_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0x807305c086A99cbDBff07cB4256cE556d9d6F0af'
),
),
Network.GNOSIS: NetworkConfig(
GNOSIS: NetworkConfig(
**asdict(BASE_NETWORKS[GNOSIS]),
VAULT_USER_LTV_TRACKER_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0xdEa72c54f63470349CE2dC12f8232FE00241abE6'
),
LEVERAGE_STRATEGY_CONTRACT_ADDRESS=ZERO_CHECKSUM_ADDRESS,
STRATEGY_REGISTRY_CONTRACT_ADDRESS=ZERO_CHECKSUM_ADDRESS,
OSTOKEN_ESCROW_CONTRACT_ADDRESS=ZERO_CHECKSUM_ADDRESS,
),
Network.CHIADO: NetworkConfig(
CHIADO: NetworkConfig(
**asdict(BASE_NETWORKS[CHIADO]),
VAULT_USER_LTV_TRACKER_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0xe0Ae8B04922d6e3fA06c2496A94EF2875EFcC7BB'
),
),
Network.SEPOLIA: NetworkConfig(
VAULT_USER_LTV_TRACKER_CONTRACT_ADDRESS=ZERO_CHECKSUM_ADDRESS,
PRICE_NETWORK_CONFIG=(
PriceNetworkConfig(
# TARGET_CHAIN is not what eth_chainId returns.
# It is internal id used in PriceFeedSender contract.
TARGET_CHAIN=10003,
# PriceFeedReceiver contract address on Arbitrum Sepolia
TARGET_ADDRESS=Web3.to_checksum_address(
'0x744836a91f5151c6ef730eb7e07c232997debaaa'
),
# PriceFeed contract address on Arbitrum Sepolia
TARGET_PRICE_FEED_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0x4026affabd9032bcc87fa05c02f088905f3dc09b'
),
# PriceFeedSender contract address on Sepolia
PRICE_FEED_SENDER_CONTRACT_ADDRESS=Web3.to_checksum_address(
'0xe572a8631a49ec4c334812bb692beecf934ac4e9'
),
)
),
LEVERAGE_STRATEGY_CONTRACT_ADDRESS=ZERO_CHECKSUM_ADDRESS,
STRATEGY_REGISTRY_CONTRACT_ADDRESS=ZERO_CHECKSUM_ADDRESS,
OSTOKEN_ESCROW_CONTRACT_ADDRESS=ZERO_CHECKSUM_ADDRESS,
),
}
9 changes: 7 additions & 2 deletions src/common/settings.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from decouple import config

from src.common.networks import NETWORKS, Network
from src.common.networks import NETWORKS

EXECUTION_ENDPOINT: str = config('EXECUTION_ENDPOINT', default='')
HOT_WALLET_PRIVATE_KEY: str = config('HOT_WALLET_PRIVATE_KEY')

GRAPH_PAGE_SIZE: int = config('GRAPH_PAGE_SIZE', default=100, cast=int)


EXECUTION_TRANSACTION_TIMEOUT: int = config('EXECUTION_TRANSACTION_TIMEOUT', default=300, cast=int)

SENTRY_DSN: str = config('SENTRY_DSN', default='')

NETWORK: Network = config('NETWORK', cast=Network)
NETWORK = config('NETWORK')
network_config = NETWORKS[NETWORK]
File renamed without changes.
Empty file added src/exit/__init__.py
Empty file.
Loading
Loading