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

[Multi-Chain] Enable payments on other chains #425

Merged
merged 25 commits into from
Dec 5, 2024
Merged
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5966dba
add additional tokens to price fetching
fhenneke Nov 8, 2024
cdf9019
add blockchain parameter to dune fetcher
fhenneke Nov 26, 2024
1ce5d8e
rename weth to wrapped native token
fhenneke Nov 18, 2024
9f5eaee
add config for other chains
fhenneke Nov 8, 2024
26ffae0
change pull request node environment variable name
fhenneke Nov 18, 2024
9222211
fix bug with missing data
fhenneke Nov 26, 2024
504ad05
fix gnosis cap
fhenneke Nov 22, 2024
e48581c
use new vouching query
fhenneke Dec 2, 2024
36d79d2
fix time range
fhenneke Dec 2, 2024
f9ec687
add exception for missing vouches
fhenneke Dec 2, 2024
6a045cf
add log for native token exchange rate
fhenneke Dec 3, 2024
52afbd5
change name of payouts safe environment variable
fhenneke Dec 3, 2024
9606888
make protocol fee safe chain dependent
fhenneke Dec 3, 2024
3d6c205
update sample .env file
fhenneke Dec 4, 2024
b2b587e
fix bug in arbitrum payment network
fhenneke Dec 4, 2024
5761990
use chain agnostic environment variables
fhenneke Dec 4, 2024
cb2be14
change vouching query id to old query
fhenneke Dec 4, 2024
e2dc829
Merge branch 'main' into other_chains
fhenneke Dec 4, 2024
ca7d981
fix CI
fhenneke Dec 5, 2024
299e963
update payout safe addresss for testing
fhenneke Dec 5, 2024
a58bf93
change pull request env variable formating
fhenneke Dec 5, 2024
98323d7
add network to transfer file
fhenneke Dec 5, 2024
8d7cd14
lint fix
fhenneke Dec 5, 2024
b35b106
add network variable to github env variables
fhenneke Dec 5, 2024
424e89a
update .env.sample
fhenneke Dec 5, 2024
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
2 changes: 1 addition & 1 deletion .env.sample
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ FILE_OUT_PATH=./out
DUNE_API_KEY=

# Safe Transaction Service Requirements.
SAFE_ADDRESS=0xA03be496e67Ec29bC62F01a428683D7F9c204930
PAYOUTS_SAFE_ADDRESS=0xA03be496e67Ec29bC62F01a428683D7F9c204930
NETWORK=mainnet
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment with the options here?
Is it {mainnet, xdai, arbitrum_one} or {ethereum, gnosis, arbitrum}?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a comment.

PROPOSER_PK=

1 change: 1 addition & 0 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
@@ -28,3 +28,4 @@ jobs:
python -m pytest tests/unit
env:
NODE_URL: https://rpc.ankr.com/eth
PAYOUTS_SAFE_ADDRESS: '0x0000000000000000000000000000000000000000'
177 changes: 124 additions & 53 deletions src/config.py
Original file line number Diff line number Diff line change
@@ -41,6 +41,8 @@ class RewardConfig:
@staticmethod
def from_network(network: Network) -> RewardConfig:
"""Initialize reward config for a given network."""
cow_bonding_pool = Address("0x5d4020b9261f01b6f8a45db929704b0ad6f5e9e6")
service_fee_factor = Fraction(15, 100)
match network:
case Network.MAINNET:
return RewardConfig(
@@ -51,10 +53,32 @@ def from_network(network: Network) -> RewardConfig:
batch_reward_cap_lower=10 * 10**15,
quote_reward_cow=6 * 10**18,
quote_reward_cap_native=6 * 10**14,
service_fee_factor=Fraction(15, 100),
cow_bonding_pool=Address(
"0x5d4020b9261f01b6f8a45db929704b0ad6f5e9e6"
service_fee_factor=service_fee_factor,
cow_bonding_pool=cow_bonding_pool,
)
case Network.GNOSIS:
return RewardConfig(
reward_token_address=Address(
"0x177127622c4a00f3d409b75571e12cb3c8973d3c"
),
batch_reward_cap_upper=30 * 10**18,
batch_reward_cap_lower=5 * 10**18,
quote_reward_cow=6 * 10**18,
quote_reward_cap_native=1 * 10**18,
service_fee_factor=service_fee_factor,
cow_bonding_pool=cow_bonding_pool,
)
case Network.ARBITRUM_ONE:
return RewardConfig(
reward_token_address=Address(
"0xcb8b5cd20bdcaea9a010ac1f8d835824f5c87a04"
),
batch_reward_cap_upper=12 * 10**15,
batch_reward_cap_lower=10 * 10**15,
quote_reward_cow=6 * 10**18,
quote_reward_cap_native=6 * 10**14,
service_fee_factor=service_fee_factor,
cow_bonding_pool=cow_bonding_pool,
)
case _:
raise ValueError(f"No reward config set up for network {network}.")
@@ -81,18 +105,27 @@ def from_network(network: Network) -> ProtocolFeeConfig:
"""Initialize protocol fee config for a given network."""
match network:
case Network.MAINNET:
return ProtocolFeeConfig(
protocol_fee_safe=Address(
"0xB64963f95215FDe6510657e719bd832BB8bb941B"
),
partner_fee_cut=0.15,
partner_fee_reduced_cut=0.10,
reduced_cut_address="0x63695Eee2c3141BDE314C5a6f89B98E62808d716",
protocol_fee_safe = Address(
"0xB64963f95215FDe6510657e719bd832BB8bb941B"
)
case Network.GNOSIS:
protocol_fee_safe = Address(
"0x6b3214fD11dc91De14718DeE98Ef59bCbFcfB432"
)
case Network.ARBITRUM_ONE:
protocol_fee_safe = Address(
"0x451100Ffc88884bde4ce87adC8bB6c7Df7fACccd"
)
case _:
raise ValueError(
f"No protocol fee config set up for network {network}."
)
return ProtocolFeeConfig(
protocol_fee_safe=protocol_fee_safe,
partner_fee_cut=0.15,
partner_fee_reduced_cut=0.10,
reduced_cut_address="0x63695Eee2c3141BDE314C5a6f89B98E62808d716",
)


@dataclass(frozen=True)
@@ -106,12 +139,18 @@ def from_network(network: Network) -> BufferAccountingConfig:
"""Initialize buffer accounting config for a given network."""
match network:
case Network.MAINNET:
return BufferAccountingConfig(include_slippage=True)
include_slippage = True
case Network.GNOSIS | Network.ARBITRUM_ONE:
include_slippage = False
case _:
raise ValueError(
f"No buffer accounting config set up for network {network}."
)

return BufferAccountingConfig(
include_slippage=include_slippage,
)


@dataclass(frozen=True)
class OrderbookConfig:
@@ -142,10 +181,16 @@ def from_network(network: Network) -> DuneConfig:
dune_api_key = os.environ.get("DUNE_API_KEY", "")
match network:
case Network.MAINNET:
return DuneConfig(dune_api_key=dune_api_key, dune_blockchain="ethereum")
dune_blockchain = "ethereum"
case Network.GNOSIS:
dune_blockchain = "gnosis"
case Network.ARBITRUM_ONE:
dune_blockchain = "arbitrum"
case _:
raise ValueError(f"No dune config set up for network {network}.")

return DuneConfig(dune_api_key=dune_api_key, dune_blockchain=dune_blockchain)


@dataclass(frozen=True)
class NodeConfig:
@@ -154,14 +199,9 @@ class NodeConfig:
node_url: str

@staticmethod
def from_network(network: Network) -> NodeConfig:
"""Initialize node config for a given network."""
match network:
case Network.MAINNET:
node_url = os.environ.get("NODE_URL", "")
case _:
raise ValueError(f"No node config set up for network {network}.")

def from_env() -> NodeConfig:
"""Initialize node config from environment variables."""
node_url = os.environ.get("NODE_URL", "")
return NodeConfig(node_url=node_url)


@@ -177,7 +217,8 @@ class PaymentConfig:
signing_key: str | None
safe_queue_url: str
verification_docs_url: str
weth_address: ChecksumAddress
wrapped_native_token_address: ChecksumAddress
wrapped_eth_address: Address

@staticmethod
def from_network(network: Network) -> PaymentConfig:
@@ -188,47 +229,79 @@ def from_network(network: Network) -> PaymentConfig:

docs_url = "https://www.notion.so/cownation/Solver-Payouts-3dfee64eb3d449ed8157a652cc817a8c"

network_short_name = {
Network.MAINNET: "eth",
Network.GNOSIS: "gno",
}
payment_safe_address = Web3.to_checksum_address(
os.environ.get(
"PAYOUTS_SAFE_ADDRESS",
"",
)
)

match network:
case Network.MAINNET:
payment_safe_address = Web3.to_checksum_address(
os.environ.get(
"SAFE_ADDRESS", "0xA03be496e67Ec29bC62F01a428683D7F9c204930"
)
payment_network = EthereumNetwork.MAINNET
short_name = "eth"

cow_token_address = Address(
"0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB"
)
wrapped_native_token_address = Web3.to_checksum_address(
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
)
short_name = network_short_name[network]
safe_url = (
f"https://app.safe.global/{short_name}:{payment_safe_address}"
wrapped_eth_address = Address(
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
)
safe_queue_url = f"{safe_url}/transactions/queue"

return PaymentConfig(
network=EthereumNetwork.MAINNET,
cow_token_address=Address(
"0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB"
),
payment_safe_address=Web3.to_checksum_address(
"0xA03be496e67Ec29bC62F01a428683D7F9c204930"
),
signing_key=signing_key,
safe_queue_url=safe_queue_url,
verification_docs_url=docs_url,
weth_address=Web3.to_checksum_address(
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
),
case Network.GNOSIS:
payment_network = EthereumNetwork.GNOSIS
short_name = "gno"

cow_token_address = Address(
"0x177127622c4a00f3d409b75571e12cb3c8973d3c"
)
wrapped_native_token_address = Web3.to_checksum_address(
"0xe91d153e0b41518a2ce8dd3d7944fa863463a97d"
)
wrapped_eth_address = Address(
"0x6a023ccd1ff6f2045c3309768ead9e68f978f6e1"
)
case Network.ARBITRUM_ONE:
payment_network = EthereumNetwork.ARBITRUM_ONE
short_name = "arb1"

cow_token_address = Address(
"0xcb8b5cd20bdcaea9a010ac1f8d835824f5c87a04"
)
wrapped_native_token_address = Web3.to_checksum_address(
"0x82af49447d8a07e3bd95bd0d56f35241523fbab1"
)
wrapped_eth_address = Address(
"0x82af49447d8a07e3bd95bd0d56f35241523fbab1"
)
case _:
raise ValueError(f"No payment config set up for network {network}.")

safe_url = f"https://app.safe.global/{short_name}:{payment_safe_address}"
safe_queue_url = f"{safe_url}/transactions/queue"

return PaymentConfig(
network=payment_network,
cow_token_address=cow_token_address,
payment_safe_address=payment_safe_address,
signing_key=signing_key,
safe_queue_url=safe_queue_url,
verification_docs_url=docs_url,
wrapped_native_token_address=wrapped_native_token_address,
wrapped_eth_address=wrapped_eth_address,
)


@dataclass(frozen=True)
class IOConfig:
"""Configuration of input and output."""

# pylint: disable=too-many-instance-attributes

network: Network
log_config_file: Path
project_root_dir: Path
query_dir: Path
@@ -240,6 +313,7 @@ class IOConfig:
@staticmethod
def from_env() -> IOConfig:
"""Initialize io config from environment variables."""
network = Network(os.getenv("NETWORK"))
slack_channel = os.getenv("SLACK_CHANNEL", None)
slack_token = os.getenv("SLACK_TOKEN", None)

@@ -250,6 +324,7 @@ def from_env() -> IOConfig:
dashboard_dir = project_root_dir / Path("dashboards/solver-rewards-accounting")

return IOConfig(
network=network,
project_root_dir=project_root_dir,
log_config_file=log_config_file,
query_dir=query_dir,
@@ -283,16 +358,12 @@ def from_network(network: Network) -> AccountingConfig:
payment_config=PaymentConfig.from_network(network),
orderbook_config=OrderbookConfig.from_env(),
dune_config=DuneConfig.from_network(network),
node_config=NodeConfig.from_network(network),
node_config=NodeConfig.from_env(),
reward_config=RewardConfig.from_network(network),
protocol_fee_config=ProtocolFeeConfig.from_network(network),
buffer_accounting_config=BufferAccountingConfig.from_network(network),
io_config=IOConfig.from_env(),
)


web3 = Web3(
Web3.HTTPProvider(
NodeConfig.from_network(Network(os.environ.get("NETWORK", "mainnet"))).node_url
)
)
web3 = Web3(Web3.HTTPProvider(NodeConfig.from_env().node_url))
13 changes: 12 additions & 1 deletion src/fetch/dune.py
Original file line number Diff line number Diff line change
@@ -22,13 +22,16 @@ class DuneFetcher:

dune: DuneClient
period: AccountingPeriod
blockchain: str

def __init__(
self,
dune: DuneClient,
blockchain: str,
period: AccountingPeriod,
):
self.dune = dune
self.blockchain = blockchain
self.period = period
# Already have period set, so we might as well store this upon construction.
# This may become an issue when we make the fetchers async;
@@ -39,6 +42,13 @@ def _period_params(self) -> list[QueryParameter]:
"""Easier access to these parameters."""
return self.period.as_query_params()

def _network_and_period_params(self) -> list[QueryParameter]:
"""Easier access to parameters for network and accounting period."""
network_param = QueryParameter.text_type("blockchain", self.blockchain)
period_params = self._period_params()

return period_params + [network_param]

@staticmethod
def _parameterized_query(
query_data: QueryData, params: list[QueryParameter]
@@ -71,7 +81,7 @@ def get_block_interval(self) -> tuple[str, str]:
"""Returns block numbers corresponding to date interval"""
results = self._get_query_results(
self._parameterized_query(
QUERIES["PERIOD_BLOCK_INTERVAL"], self._period_params()
QUERIES["PERIOD_BLOCK_INTERVAL"], self._network_and_period_params()
)
)
assert len(results) == 1, "Block Interval Query should return only 1 result!"
@@ -87,6 +97,7 @@ def get_vouches(self) -> list[DuneRecord]:
params=[
QueryParameter.date_type("end_time", self.period.end),
QueryParameter.enum_type("vouch_cte_name", "named_results"),
QueryParameter.text_type("blockchain", self.blockchain),
],
)
)
19 changes: 14 additions & 5 deletions src/fetch/payouts.py
Original file line number Diff line number Diff line change
@@ -516,7 +516,13 @@ def construct_payouts(
for service_fee_flag in service_fee_df["service_fee"]
]

reward_target_df = pandas.DataFrame(dune.get_vouches())
vouches = dune.get_vouches()
if vouches:
reward_target_df = pandas.DataFrame(dune.get_vouches())
else:
reward_target_df = DataFrame(
columns=["solver", "solver_name", "reward_target", "pool_address"]
)
# construct slippage df
if ignore_slippage_flag or (not config.buffer_accounting_config.include_slippage):
slippage_df_temp = pandas.merge(
@@ -534,13 +540,14 @@ def construct_payouts(
slippage_df = slippage_df.rename(columns={"solver_address": "solver"})

reward_token = config.reward_config.reward_token_address
native_token = Address(config.payment_config.weth_address)
native_token = Address(config.payment_config.wrapped_native_token_address)
wrapped_eth = config.payment_config.wrapped_eth_address
price_day = dune.period.end - timedelta(days=1)
exchange_rate_native_to_cow = exchange_rate_atoms(
native_token, reward_token, price_day
)
log.info(
f"An exchange rate of {exchange_rate_native_to_cow:.4f} COW/native token is used."
exchange_rate_native_to_eth = exchange_rate_atoms(
native_token, wrapped_eth, price_day
)
converter = TokenConversion(
eth_to_token=lambda t: exchange_rate_native_to_cow * t,
@@ -587,7 +594,9 @@ def construct_payouts(
f"Protocol Fees: {final_protocol_fee_wei / 10 ** 18:.4f}\n"
f"Partner Fees Tax: {partner_fee_tax_wei / 10 ** 18:.4f}\n"
f"Partner Fees: {total_partner_fee_wei_taxed / 10 ** 18:.4f}\n"
f"COW DAO Service Fees: {service_fee / 10 ** 18:.4f}\n",
f"COW DAO Service Fees: {service_fee / 10 ** 18:.4f}\n\n"
f"Exchange rate native token to COW: {exchange_rate_native_to_cow:.4f} COW/native token\n"
f"Exchange rate native token to ETH: {exchange_rate_native_to_eth:.4f} ETH/native token\n",
category=Category.TOTALS,
)
payouts = prepare_transfers(
11 changes: 10 additions & 1 deletion src/fetch/prices.py
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ class TokenId(Enum):
"""Coin Ids for coin paprika"""

ETH = "eth-ethereum"
XDAI = "xdai-xdai"
COW = "cow-cow-protocol-token"
USDC = "usdc-usd-coin"

@@ -40,9 +41,17 @@ def decimals(self) -> int:


TOKEN_ADDRESS_TO_ID = {
Address("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"): TokenId.ETH,
# mainnet tokens
Address("0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB"): TokenId.COW,
Address("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"): TokenId.ETH,
Address("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"): TokenId.USDC,
# gnosis tokens
Address("0x177127622c4a00f3d409b75571e12cb3c8973d3c"): TokenId.COW,
Address("0x6a023ccd1ff6f2045c3309768ead9e68f978f6e1"): TokenId.ETH,
Address("0xe91d153e0b41518a2ce8dd3d7944fa863463a97d"): TokenId.XDAI,
# arbitrum tokens
Address("0xcb8b5cd20bdcaea9a010ac1f8d835824f5c87a04"): TokenId.COW,
Address("0x82af49447d8a07e3bd95bd0d56f35241523fbab1"): TokenId.ETH,
}


5 changes: 3 additions & 2 deletions src/fetch/transfer_file.py
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@ def manual_propose(
)
csv_transfers = [asdict(CSVTransfer.from_transfer(t)) for t in transfers]
FileIO(config.io_config.csv_output_dir).write_csv(
csv_transfers, f"transfers-{period}.csv"
csv_transfers, f"transfers-{config.io_config.network.value}-{period}.csv"
)

print(Transfer.summarize(transfers))
@@ -75,7 +75,7 @@ def auto_propose(
transactions = prepend_unwrap_if_necessary(
client,
config.payment_config.payment_safe_address,
wrapped_native_token=config.payment_config.weth_address,
wrapped_native_token=config.payment_config.wrapped_native_token_address,
transactions=[t.as_multisend_tx() for t in transfers],
)
if len(transactions) > len(transfers):
@@ -124,6 +124,7 @@ def main() -> None:
)
dune = DuneFetcher(
dune=DuneClient(config.dune_config.dune_api_key),
blockchain=config.dune_config.dune_blockchain,
period=accounting_period,
)

2 changes: 1 addition & 1 deletion src/queries.py
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ def with_params(self, params: list[QueryParameter]) -> QueryBase:
QUERIES = {
"PERIOD_BLOCK_INTERVAL": QueryData(
name="Block Interval for Accounting Period",
q_id=3333356,
q_id=4227027,
),
"VOUCH_REGISTRY": QueryData(
name="Vouch Registry",
1 change: 1 addition & 0 deletions tests/e2e/test_get_block_number.py
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ def setUp(self) -> None:
load_dotenv()
self.fetcher = DuneFetcher(
DuneClient(os.environ["DUNE_API_KEY"]),
"ethereum",
AccountingPeriod("2022-10-18"),
)

4 changes: 3 additions & 1 deletion tests/e2e/test_prices.py
Original file line number Diff line number Diff line change
@@ -22,7 +22,9 @@ def setUp(self) -> None:
self.eth_price = usd_price(TokenId.ETH, self.some_date)
self.usdc_price = usd_price(TokenId.USDC, self.some_date)
self.cow_address = self.config.reward_config.reward_token_address
self.weth_address = Address(self.config.payment_config.weth_address)
self.weth_address = Address(
self.config.payment_config.wrapped_native_token_address
)
self.usdc_address = Address("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")

def test_usd_price(self):
6 changes: 3 additions & 3 deletions tests/unit/test_multisend.py
Original file line number Diff line number Diff line change
@@ -31,11 +31,11 @@ def test_prepend_unwrap(self):
client=self.client,
safe_address=safe_address,
transactions=[big_native_transfer],
wrapped_native_token=self.payment_config.weth_address,
wrapped_native_token=self.payment_config.wrapped_native_token_address,
)

eth_balance = self.client.get_balance(safe_address)
weth = weth9(self.client.w3, self.payment_config.weth_address)
weth = weth9(self.client.w3, self.payment_config.wrapped_native_token_address)
weth_balance = weth.functions.balanceOf(safe_address).call()

transactions = [
@@ -49,7 +49,7 @@ def test_prepend_unwrap(self):
self.client,
safe_address,
transactions,
self.payment_config.weth_address,
self.payment_config.wrapped_native_token_address,
skip_validation=True,
)