Skip to content

Commit

Permalink
Support other networks than Mainnet for Sushiswap
Browse files Browse the repository at this point in the history
- Organize Sushiswap and Kyber oracles files
- Related to #343
  • Loading branch information
Uxio0 committed Sep 2, 2022
1 parent 9302d5e commit 6587f38
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 162 deletions.
4 changes: 2 additions & 2 deletions gnosis/eth/oracles/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# flake8: noqa F401
from .kyber import KyberOracle
from .oracles import (
AaveOracle,
BalancerOracle,
Expand All @@ -8,20 +9,19 @@
CurveOracle,
EnzymeOracle,
InvalidPriceFromOracle,
KyberOracle,
MooniswapOracle,
OracleException,
PoolTogetherOracle,
PriceOracle,
PricePoolOracle,
SushiswapOracle,
UnderlyingToken,
UniswapOracle,
UniswapV2Oracle,
UsdPricePoolOracle,
YearnOracle,
ZerionComposedOracle,
)
from .sushiswap import SushiswapOracle
from .uniswap_v3 import UniswapV3Oracle

__all__ = [
Expand Down
102 changes: 102 additions & 0 deletions gnosis/eth/oracles/kyber.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import logging
from typing import Optional

from eth_abi.exceptions import DecodingError
from web3.exceptions import BadFunctionCallOutput

from gnosis.util import cached_property

from .. import EthereumClient, EthereumNetwork
from ..contracts import get_kyber_network_proxy_contract
from .oracles import CannotGetPriceFromOracle, InvalidPriceFromOracle, PriceOracle

logger = logging.getLogger(__name__)


class KyberOracle(PriceOracle):
"""
KyberSwap Legacy Oracle
https://docs.kyberswap.com/Legacy/addresses/addresses-mainnet
"""

# This is the `tokenAddress` they use for ETH ¯\_(ツ)_/¯
ETH_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
ADDRESSES = {
EthereumNetwork.MAINNET: "0x9AAb3f75489902f3a48495025729a0AF77d4b11e",
EthereumNetwork.RINKEBY: "0x0d5371e5EE23dec7DF251A8957279629aa79E9C5",
EthereumNetwork.ROPSTEN: "0xd719c34261e099Fdb33030ac8909d5788D3039C4",
EthereumNetwork.KOVAN: "0xc153eeAD19e0DBbDb3462Dcc2B703cC6D738A37c",
}

def __init__(
self,
ethereum_client: EthereumClient,
kyber_network_proxy_address: Optional[str] = None,
):
"""
:param ethereum_client:
:param kyber_network_proxy_address: https://developer.kyber.network/docs/MainnetEnvGuide/#contract-addresses
"""
self.ethereum_client = ethereum_client
self.w3 = ethereum_client.w3
self._kyber_network_proxy_address = kyber_network_proxy_address

@cached_property
def kyber_network_proxy_address(self):
if self._kyber_network_proxy_address:
return self._kyber_network_proxy_address
return self.ADDRESSES.get(
self.ethereum_client.get_network(),
self.ADDRESSES.get(EthereumNetwork.MAINNET),
) # By default return Mainnet address

@cached_property
def kyber_network_proxy_contract(self):
return get_kyber_network_proxy_contract(
self.w3, self.kyber_network_proxy_address
)

def get_price(
self, token_address_1: str, token_address_2: str = ETH_TOKEN_ADDRESS
) -> float:
if token_address_1 == token_address_2:
return 1.0
try:
# Get decimals for token, estimation will be more accurate
decimals = self.ethereum_client.erc20.get_decimals(token_address_1)
token_unit = int(10**decimals)
(
expected_rate,
_,
) = self.kyber_network_proxy_contract.functions.getExpectedRate(
token_address_1, token_address_2, int(token_unit)
).call()

price = expected_rate / 1e18

if price <= 0.0:
# Try again the opposite
(
expected_rate,
_,
) = self.kyber_network_proxy_contract.functions.getExpectedRate(
token_address_2, token_address_1, int(token_unit)
).call()
price = (token_unit / expected_rate) if expected_rate else 0

if price <= 0.0:
error_message = (
f"price={price} <= 0 from kyber-network-proxy={self.kyber_network_proxy_address} "
f"for token-1={token_address_1} to token-2={token_address_2}"
)
logger.warning(error_message)
raise InvalidPriceFromOracle(error_message)
return price
except (ValueError, BadFunctionCallOutput, DecodingError) as e:
error_message = (
f"Cannot get price from kyber-network-proxy={self.kyber_network_proxy_address} "
f"for token-1={token_address_1} to token-2={token_address_2}"
)
logger.warning(error_message)
raise CannotGetPriceFromOracle(error_message) from e
107 changes: 10 additions & 97 deletions gnosis/eth/oracles/oracles.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from ..constants import NULL_ADDRESS
from ..contracts import (
get_erc20_contract,
get_kyber_network_proxy_contract,
get_uniswap_factory_contract,
get_uniswap_v2_factory_contract,
get_uniswap_v2_pair_contract,
Expand Down Expand Up @@ -77,89 +76,6 @@ def get_underlying_tokens(self, *args) -> List[Tuple[UnderlyingToken]]:
pass


class KyberOracle(PriceOracle):
# This is the `tokenAddress` they use for ETH ¯\_(ツ)_/¯
ETH_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
ADDRESSES = {
EthereumNetwork.MAINNET: "0x9AAb3f75489902f3a48495025729a0AF77d4b11e",
EthereumNetwork.RINKEBY: "0x0d5371e5EE23dec7DF251A8957279629aa79E9C5",
EthereumNetwork.ROPSTEN: "0xd719c34261e099Fdb33030ac8909d5788D3039C4",
EthereumNetwork.KOVAN: "0xc153eeAD19e0DBbDb3462Dcc2B703cC6D738A37c",
}

def __init__(
self,
ethereum_client: EthereumClient,
kyber_network_proxy_address: Optional[str] = None,
):
"""
:param ethereum_client:
:param kyber_network_proxy_address: https://developer.kyber.network/docs/MainnetEnvGuide/#contract-addresses
"""
self.ethereum_client = ethereum_client
self.w3 = ethereum_client.w3
self._kyber_network_proxy_address = kyber_network_proxy_address

@cached_property
def kyber_network_proxy_address(self):
if self._kyber_network_proxy_address:
return self._kyber_network_proxy_address
return self.ADDRESSES.get(
self.ethereum_client.get_network(),
self.ADDRESSES.get(EthereumNetwork.MAINNET),
) # By default return Mainnet address

@cached_property
def kyber_network_proxy_contract(self):
return get_kyber_network_proxy_contract(
self.w3, self.kyber_network_proxy_address
)

def get_price(
self, token_address_1: str, token_address_2: str = ETH_TOKEN_ADDRESS
) -> float:
if token_address_1 == token_address_2:
return 1.0
try:
# Get decimals for token, estimation will be more accurate
decimals = self.ethereum_client.erc20.get_decimals(token_address_1)
token_unit = int(10**decimals)
(
expected_rate,
_,
) = self.kyber_network_proxy_contract.functions.getExpectedRate(
token_address_1, token_address_2, int(token_unit)
).call()

price = expected_rate / 1e18

if price <= 0.0:
# Try again the opposite
(
expected_rate,
_,
) = self.kyber_network_proxy_contract.functions.getExpectedRate(
token_address_2, token_address_1, int(token_unit)
).call()
price = (token_unit / expected_rate) if expected_rate else 0

if price <= 0.0:
error_message = (
f"price={price} <= 0 from kyber-network-proxy={self.kyber_network_proxy_address} "
f"for token-1={token_address_1} to token-2={token_address_2}"
)
logger.warning(error_message)
raise InvalidPriceFromOracle(error_message)
return price
except (ValueError, BadFunctionCallOutput, DecodingError) as e:
error_message = (
f"Cannot get price from kyber-network-proxy={self.kyber_network_proxy_address} "
f"for token-1={token_address_1} to token-2={token_address_2}"
)
logger.warning(error_message)
raise CannotGetPriceFromOracle(error_message) from e


class UniswapOracle(PriceOracle):
ADDRESSES = {
EthereumNetwork.MAINNET: "0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95",
Expand Down Expand Up @@ -299,13 +215,14 @@ def get_price(self, token_address: str) -> float:


class UniswapV2Oracle(PricePoolOracle, PriceOracle):
ROUTER_ADDRESSES = {
EthereumNetwork.MAINNET: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
}

# Pair init code is keccak(getCode(UniswapV2Pair))
pair_init_code = HexBytes(
PAIR_INIT_CODE = HexBytes(
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f"
)
router_address = (
"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D" # Same address on every network
)

def __init__(
self, ethereum_client: EthereumClient, router_address: Optional[str] = None
Expand All @@ -316,7 +233,10 @@ def __init__(
"""
self.ethereum_client = ethereum_client
self.w3 = ethereum_client.w3
self.router_address: str = router_address or self.router_address
self.router_address: str = router_address or self.ROUTER_ADDRESSES.get(
ethereum_client.get_network(),
self.ROUTER_ADDRESSES[EthereumNetwork.MAINNET],
)
self.router = get_uniswap_v2_router_contract(
ethereum_client.w3, self.router_address
)
Expand Down Expand Up @@ -383,7 +303,7 @@ def calculate_pair_address(self, token_address: str, token_address_2: str):
address = fast_keccak(
encode_abi_packed(
["bytes", "address", "bytes", "bytes"],
[HexBytes("ff"), self.factory_address, salt, self.pair_init_code],
[HexBytes("ff"), self.factory_address, salt, self.PAIR_INIT_CODE],
)
)[-20:]
return fast_bytes_to_checksum_address(address)
Expand Down Expand Up @@ -532,13 +452,6 @@ def get_pool_token_price(self, pool_token_address: ChecksumAddress) -> float:
raise CannotGetPriceFromOracle(error_message) from e


class SushiswapOracle(UniswapV2Oracle):
pair_init_code = HexBytes(
"0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303"
)
router_address = "0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F"


class AaveOracle(PriceOracle):
def __init__(self, ethereum_client: EthereumClient, price_oracle: PriceOracle):
"""
Expand Down
30 changes: 30 additions & 0 deletions gnosis/eth/oracles/sushiswap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import logging

from hexbytes import HexBytes

from .. import EthereumNetwork
from .oracles import UniswapV2Oracle

logger = logging.getLogger(__name__)


class SushiswapOracle(UniswapV2Oracle):
PAIR_INIT_CODE = HexBytes(
"0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303"
)
ROUTER_ADDRESSES = {
EthereumNetwork.MAINNET: "0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F",
EthereumNetwork.MATIC: "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506",
EthereumNetwork.ARBITRUM: "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506",
EthereumNetwork.AVALANCHE: "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506",
EthereumNetwork.MOON_MOONRIVER: "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506",
EthereumNetwork.FANTOM: "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506",
EthereumNetwork.BINANCE: "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506",
EthereumNetwork.XDAI: "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506",
EthereumNetwork.CELO: "0x1421bDe4B10e8dd459b3BCb598810B1337D56842",
EthereumNetwork.FUSE_MAINNET: "0xF4d73326C13a4Fc5FD7A064217e12780e9Bd62c3",
EthereumNetwork.OKEXCHAIN: "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506",
EthereumNetwork.PALM: "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506",
EthereumNetwork.MOON_MOONBEAM: "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506",
EthereumNetwork.HECO: "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506",
}
49 changes: 49 additions & 0 deletions gnosis/eth/tests/oracles/test_kyber.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from django.test import TestCase

from eth_account import Account

from ... import EthereumClient
from ...oracles import CannotGetPriceFromOracle, KyberOracle
from ..ethereum_test_case import EthereumTestCaseMixin
from ..test_oracles import (
dai_token_mainnet_address,
gno_token_mainnet_address,
usdt_token_mainnet_address,
weth_token_mainnet_address,
)
from ..utils import just_test_if_mainnet_node


class TestKyberOracle(EthereumTestCaseMixin, TestCase):
def test_kyber_oracle(self):
mainnet_node = just_test_if_mainnet_node()
ethereum_client = EthereumClient(mainnet_node)
kyber_oracle = KyberOracle(ethereum_client)
price = kyber_oracle.get_price(
gno_token_mainnet_address, weth_token_mainnet_address
)
self.assertLess(price, 1)
self.assertGreater(price, 0)

# Test with 2 stablecoins
price = kyber_oracle.get_price(
dai_token_mainnet_address, usdt_token_mainnet_address
)
self.assertAlmostEqual(price, 1.0, delta=0.5)

price = kyber_oracle.get_price(
usdt_token_mainnet_address, dai_token_mainnet_address
)
self.assertAlmostEqual(price, 1.0, delta=0.5)

self.assertEqual(
kyber_oracle.get_price("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"), 1.0
)

def test_kyber_oracle_not_deployed(self):
kyber_oracle = KyberOracle(self.ethereum_client, Account.create().address)
random_token_address = Account.create().address
with self.assertRaisesMessage(
CannotGetPriceFromOracle, "Cannot get price from kyber-network-proxy"
):
kyber_oracle.get_price(random_token_address)
Loading

0 comments on commit 6587f38

Please sign in to comment.