diff --git a/fee_allocator/abi/laposte.json b/fee_allocator/abi/laposte.json new file mode 100644 index 00000000..8d2c5a0d --- /dev/null +++ b/fee_allocator/abi/laposte.json @@ -0,0 +1,21 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + } + ], + "name": "sentNonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/fee_allocator/bribe_platforms/stakedao.py b/fee_allocator/bribe_platforms/stakedao.py index d076020e..72f2271b 100644 --- a/fee_allocator/bribe_platforms/stakedao.py +++ b/fee_allocator/bribe_platforms/stakedao.py @@ -12,11 +12,19 @@ import os -AURA_VEBAL_LOCKER = Web3.to_checksum_address("0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2") - - class StakeDAOPlatform(BribePlatform): SUPPORTED_L2_CHAINS = ["arbitrum", "optimism", "base", "polygon"] + AURA_VEBAL_LOCKER = Web3.to_checksum_address("0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2") + BASE_GAS_LIMIT = 50000 + GAS_BUFFER_MULTIPLIER = 1.25 + ABI_DIR = Path(__file__).parent.parent / "abi" + CCIP_ROUTERS = { + "mainnet": Web3.to_checksum_address("0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D"), + "arbitrum": Web3.to_checksum_address("0x141fa059441E0ca23ce184B6A78bafD2A517DdE8"), + "optimism": Web3.to_checksum_address("0x3206695CaE29952f4b0c22a169725a865bc8Ce0f"), + "base": Web3.to_checksum_address("0x881e3A65B4d4a04dD529061dd0071cf975F58bCD"), + "polygon": Web3.to_checksum_address("0x849c5ED5a80F5B408Dd4969b78c2C8fdf0565Bfe"), + } def __init__(self, book: Dict[str, str], run_config: Any): super().__init__(book, run_config) @@ -38,10 +46,12 @@ def _build_gauge_to_chain_map(self): if pool.gauge_address: self._gauge_to_chain_cache[pool.gauge_address.lower()] = chain.name + def _load_abi(self, name: str): + with open(self.ABI_DIR / f"{name}.json", 'r') as f: + return json.load(f) + def _get_chain_selector(self, chain_id: int) -> int: - base_dir = Path(__file__).parent.parent - with open(f"{base_dir}/abi/laposte_adapter.json", 'r') as f: - adapter_abi = json.load(f) + adapter_abi = self._load_abi("laposte_adapter") adapter_contract = self.w3.eth.contract( address=Web3.to_checksum_address(self.laposte_adapter_address), @@ -49,43 +59,88 @@ def _get_chain_selector(self, chain_id: int) -> int: ) try: - selector = adapter_contract.functions.getBridgeChainId(chain_id).call() - return selector + return adapter_contract.functions.getBridgeChainId(chain_id).call() except Exception as e: logger.error(f"Failed to get chain selector for chain {chain_id}: {e}") raise - def _calculate_ccip_fee(self, destination_chain_id: int, campaign_params: tuple) -> int: - destination_selector = self._get_chain_selector(destination_chain_id) - - base_dir = Path(__file__).parent.parent - with open(f"{base_dir}/abi/ccip_router.json", 'r') as f: - router_abi = json.load(f) - - router_contract = self.w3.eth.contract( - address=self.ccip_router_address, - abi=router_abi - ) - - payload_data = encode( + def _build_laposte_message(self, destination_chain_id: int, campaign_params: tuple, votemarket_address: str, sender_address: str) -> bytes: + payload_params = encode( ['(uint256,address,address,address,uint8,uint256,uint256,address[],address,bool)'], [campaign_params] ) + payload = encode( + ['(uint8,address,address,bytes)'], + [(0, sender_address, votemarket_address, payload_params)] + ) - laposte_message = encode( - ['(uint256,address,address,(address,uint256)[],bytes)'], + token = self.w3.eth.contract(address=Web3.to_checksum_address(self.usdc_address), abi=self._load_abi("ERC20")) + token_name = token.functions.name().call() + token_symbol = token.functions.symbol().call() + token_decimals = token.functions.decimals().call() + + laposte = self.w3.eth.contract(address=Web3.to_checksum_address(self.laposte_address), abi=self._load_abi("laposte")) + nonce = laposte.functions.sentNonces(destination_chain_id).call() + 1 + + return encode( + ['(uint256,address,address,(address,uint256)[],(string,string,uint8)[],bytes,uint256)'], [( destination_chain_id, self.campaign_remote_manager_address, self.campaign_remote_manager_address, [(self.usdc_address, campaign_params[6])], - payload_data + [(token_name, token_symbol, token_decimals)], + payload, + nonce )] ) - gas_limit = 200000 + def _simulate_ccip_receive(self, destination_chain_name: str, laposte_message: bytes) -> int: + dest_w3 = Web3Rpc(destination_chain_name, os.environ.get("DRPC_KEY")) + source_chain_selector = self._get_chain_selector(1) + + message_id = Web3.keccak(text='gas_estimation') + any2evm_message = ( + message_id, + source_chain_selector, + encode(['address'], [self.laposte_address]), + laposte_message, + [] + ) + + ccip_receive_sig = Web3.keccak(text='ccipReceive((bytes32,uint64,bytes,bytes,(address,uint256)[]))').hex()[:8] + encoded_params = encode( + ['(bytes32,uint64,bytes,bytes,(address,uint256)[])'], + [any2evm_message] + ) + calldata = bytes.fromhex(ccip_receive_sig) + encoded_params + + estimated_gas = dest_w3.eth.estimate_gas({ + 'from': self.CCIP_ROUTERS[destination_chain_name], + 'to': self.laposte_adapter_address, + 'data': '0x' + calldata.hex() + }) + + base_gas = estimated_gas - self.BASE_GAS_LIMIT + additional_gas_limit = int(base_gas * self.GAS_BUFFER_MULTIPLIER) + + logger.info(f"CCIP gas estimation: base={base_gas}, with_buffer={additional_gas_limit}, total={additional_gas_limit + self.BASE_GAS_LIMIT}") + return additional_gas_limit + + def _calculate_ccip_fee(self, destination_chain_id: int, destination_chain_name: str, campaign_params: tuple, votemarket_address: str, sender_address: str) -> Tuple[int, int]: + destination_selector = self._get_chain_selector(destination_chain_id) + + router_contract = self.w3.eth.contract( + address=self.ccip_router_address, + abi=self._load_abi("ccip_router") + ) + + laposte_message = self._build_laposte_message(destination_chain_id, campaign_params, votemarket_address, sender_address) + additional_gas_limit = self._simulate_ccip_receive(destination_chain_name, laposte_message) + total_gas_limit = additional_gas_limit + self.BASE_GAS_LIMIT + evm_extra_args_tag = bytes.fromhex('97a657c9') - extra_args_data = encode(['uint256'], [gas_limit]) + extra_args_data = encode(['uint256'], [total_gas_limit]) evm_extra_args = evm_extra_args_tag + extra_args_data ccip_message = { @@ -101,20 +156,19 @@ def _calculate_ccip_fee(self, destination_chain_id: int, campaign_params: tuple) ccip_message ).call() - fee_with_buffer = int(fee * 1.50) + fee_with_buffer = int(fee * 1.5) - logger.info(f"CCIP fee for chain {destination_chain_id}: {Web3.from_wei(fee_with_buffer, 'ether')} ETH (with 50% buffer)") - return fee_with_buffer + logger.info(f"CCIP fee for chain {destination_chain_id}: {Web3.from_wei(fee_with_buffer, 'ether')} ETH (gas_limit={total_gas_limit})") + return fee_with_buffer, additional_gas_limit def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> None: if bribes_df.empty or bribes_df["amount"].sum() == 0: logger.info("No bribes to process for StakeDAO") return - base_dir = Path(__file__).parent.parent campaign_manager = SafeContract( self.campaign_remote_manager_address, - abi_file_path=f"{base_dir}/abi/stakedao_marketv2.json" + abi_file_path=str(self.ABI_DIR / "stakedao_marketv2.json") ) total_usdc = sum(int(row["amount"] * 1e6) for _, row in bribes_df.iterrows() if row["amount"] > 0) @@ -152,7 +206,7 @@ def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> No aura_only = is_alliance or voting_override == "aura" bal_only = voting_override == "bal" - addresses = [AURA_VEBAL_LOCKER] if aura_only or bal_only else [] + addresses = [self.AURA_VEBAL_LOCKER] if aura_only or bal_only else [] is_whitelist = aura_only campaign_params = ( @@ -168,12 +222,12 @@ def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> No is_whitelist, ) - ccip_fee = self._calculate_ccip_fee(destination_chain_id, campaign_params) + ccip_fee, additional_gas_limit = self._calculate_ccip_fee(destination_chain_id, destination_chain_name, campaign_params, vote_market_v2_address, builder.safe_address) campaign_manager.createCampaign( campaign_params, destination_chain_id, - 0, + additional_gas_limit, vote_market_v2_address, value=ccip_fee )