Skip to content
This repository was archived by the owner on Apr 14, 2026. It is now read-only.

Commit 9d4477e

Browse files
authored
Merge pull request #282 from BalancerMaxis/fix/ccip-gas-estimation
fix ccip gas limit issue
2 parents 644b8ca + d8abfab commit 9d4477e

2 files changed

Lines changed: 109 additions & 34 deletions

File tree

fee_allocator/abi/laposte.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[
2+
{
3+
"inputs": [
4+
{
5+
"internalType": "uint256",
6+
"name": "chainId",
7+
"type": "uint256"
8+
}
9+
],
10+
"name": "sentNonces",
11+
"outputs": [
12+
{
13+
"internalType": "uint256",
14+
"name": "",
15+
"type": "uint256"
16+
}
17+
],
18+
"stateMutability": "view",
19+
"type": "function"
20+
}
21+
]

fee_allocator/bribe_platforms/stakedao.py

Lines changed: 88 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,19 @@
1212
import os
1313

1414

15-
AURA_VEBAL_LOCKER = Web3.to_checksum_address("0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2")
16-
17-
1815
class StakeDAOPlatform(BribePlatform):
1916
SUPPORTED_L2_CHAINS = ["arbitrum", "optimism", "base", "polygon"]
17+
AURA_VEBAL_LOCKER = Web3.to_checksum_address("0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2")
18+
BASE_GAS_LIMIT = 50000
19+
GAS_BUFFER_MULTIPLIER = 1.25
20+
ABI_DIR = Path(__file__).parent.parent / "abi"
21+
CCIP_ROUTERS = {
22+
"mainnet": Web3.to_checksum_address("0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D"),
23+
"arbitrum": Web3.to_checksum_address("0x141fa059441E0ca23ce184B6A78bafD2A517DdE8"),
24+
"optimism": Web3.to_checksum_address("0x3206695CaE29952f4b0c22a169725a865bc8Ce0f"),
25+
"base": Web3.to_checksum_address("0x881e3A65B4d4a04dD529061dd0071cf975F58bCD"),
26+
"polygon": Web3.to_checksum_address("0x849c5ED5a80F5B408Dd4969b78c2C8fdf0565Bfe"),
27+
}
2028

2129
def __init__(self, book: Dict[str, str], run_config: Any):
2230
super().__init__(book, run_config)
@@ -38,54 +46,101 @@ def _build_gauge_to_chain_map(self):
3846
if pool.gauge_address:
3947
self._gauge_to_chain_cache[pool.gauge_address.lower()] = chain.name
4048

49+
def _load_abi(self, name: str):
50+
with open(self.ABI_DIR / f"{name}.json", 'r') as f:
51+
return json.load(f)
52+
4153
def _get_chain_selector(self, chain_id: int) -> int:
42-
base_dir = Path(__file__).parent.parent
43-
with open(f"{base_dir}/abi/laposte_adapter.json", 'r') as f:
44-
adapter_abi = json.load(f)
54+
adapter_abi = self._load_abi("laposte_adapter")
4555

4656
adapter_contract = self.w3.eth.contract(
4757
address=Web3.to_checksum_address(self.laposte_adapter_address),
4858
abi=adapter_abi
4959
)
5060

5161
try:
52-
selector = adapter_contract.functions.getBridgeChainId(chain_id).call()
53-
return selector
62+
return adapter_contract.functions.getBridgeChainId(chain_id).call()
5463
except Exception as e:
5564
logger.error(f"Failed to get chain selector for chain {chain_id}: {e}")
5665
raise
5766

58-
def _calculate_ccip_fee(self, destination_chain_id: int, campaign_params: tuple) -> int:
59-
destination_selector = self._get_chain_selector(destination_chain_id)
60-
61-
base_dir = Path(__file__).parent.parent
62-
with open(f"{base_dir}/abi/ccip_router.json", 'r') as f:
63-
router_abi = json.load(f)
64-
65-
router_contract = self.w3.eth.contract(
66-
address=self.ccip_router_address,
67-
abi=router_abi
68-
)
69-
70-
payload_data = encode(
67+
def _build_laposte_message(self, destination_chain_id: int, campaign_params: tuple, votemarket_address: str, sender_address: str) -> bytes:
68+
payload_params = encode(
7169
['(uint256,address,address,address,uint8,uint256,uint256,address[],address,bool)'],
7270
[campaign_params]
7371
)
72+
payload = encode(
73+
['(uint8,address,address,bytes)'],
74+
[(0, sender_address, votemarket_address, payload_params)]
75+
)
7476

75-
laposte_message = encode(
76-
['(uint256,address,address,(address,uint256)[],bytes)'],
77+
token = self.w3.eth.contract(address=Web3.to_checksum_address(self.usdc_address), abi=self._load_abi("ERC20"))
78+
token_name = token.functions.name().call()
79+
token_symbol = token.functions.symbol().call()
80+
token_decimals = token.functions.decimals().call()
81+
82+
laposte = self.w3.eth.contract(address=Web3.to_checksum_address(self.laposte_address), abi=self._load_abi("laposte"))
83+
nonce = laposte.functions.sentNonces(destination_chain_id).call() + 1
84+
85+
return encode(
86+
['(uint256,address,address,(address,uint256)[],(string,string,uint8)[],bytes,uint256)'],
7787
[(
7888
destination_chain_id,
7989
self.campaign_remote_manager_address,
8090
self.campaign_remote_manager_address,
8191
[(self.usdc_address, campaign_params[6])],
82-
payload_data
92+
[(token_name, token_symbol, token_decimals)],
93+
payload,
94+
nonce
8395
)]
8496
)
8597

86-
gas_limit = 200000
98+
def _simulate_ccip_receive(self, destination_chain_name: str, laposte_message: bytes) -> int:
99+
dest_w3 = Web3Rpc(destination_chain_name, os.environ.get("DRPC_KEY"))
100+
source_chain_selector = self._get_chain_selector(1)
101+
102+
message_id = Web3.keccak(text='gas_estimation')
103+
any2evm_message = (
104+
message_id,
105+
source_chain_selector,
106+
encode(['address'], [self.laposte_address]),
107+
laposte_message,
108+
[]
109+
)
110+
111+
ccip_receive_sig = Web3.keccak(text='ccipReceive((bytes32,uint64,bytes,bytes,(address,uint256)[]))').hex()[:8]
112+
encoded_params = encode(
113+
['(bytes32,uint64,bytes,bytes,(address,uint256)[])'],
114+
[any2evm_message]
115+
)
116+
calldata = bytes.fromhex(ccip_receive_sig) + encoded_params
117+
118+
estimated_gas = dest_w3.eth.estimate_gas({
119+
'from': self.CCIP_ROUTERS[destination_chain_name],
120+
'to': self.laposte_adapter_address,
121+
'data': '0x' + calldata.hex()
122+
})
123+
124+
base_gas = estimated_gas - self.BASE_GAS_LIMIT
125+
additional_gas_limit = int(base_gas * self.GAS_BUFFER_MULTIPLIER)
126+
127+
logger.info(f"CCIP gas estimation: base={base_gas}, with_buffer={additional_gas_limit}, total={additional_gas_limit + self.BASE_GAS_LIMIT}")
128+
return additional_gas_limit
129+
130+
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]:
131+
destination_selector = self._get_chain_selector(destination_chain_id)
132+
133+
router_contract = self.w3.eth.contract(
134+
address=self.ccip_router_address,
135+
abi=self._load_abi("ccip_router")
136+
)
137+
138+
laposte_message = self._build_laposte_message(destination_chain_id, campaign_params, votemarket_address, sender_address)
139+
additional_gas_limit = self._simulate_ccip_receive(destination_chain_name, laposte_message)
140+
total_gas_limit = additional_gas_limit + self.BASE_GAS_LIMIT
141+
87142
evm_extra_args_tag = bytes.fromhex('97a657c9')
88-
extra_args_data = encode(['uint256'], [gas_limit])
143+
extra_args_data = encode(['uint256'], [total_gas_limit])
89144
evm_extra_args = evm_extra_args_tag + extra_args_data
90145

91146
ccip_message = {
@@ -101,20 +156,19 @@ def _calculate_ccip_fee(self, destination_chain_id: int, campaign_params: tuple)
101156
ccip_message
102157
).call()
103158

104-
fee_with_buffer = int(fee * 1.50)
159+
fee_with_buffer = int(fee * 1.5)
105160

106-
logger.info(f"CCIP fee for chain {destination_chain_id}: {Web3.from_wei(fee_with_buffer, 'ether')} ETH (with 50% buffer)")
107-
return fee_with_buffer
161+
logger.info(f"CCIP fee for chain {destination_chain_id}: {Web3.from_wei(fee_with_buffer, 'ether')} ETH (gas_limit={total_gas_limit})")
162+
return fee_with_buffer, additional_gas_limit
108163

109164
def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> None:
110165
if bribes_df.empty or bribes_df["amount"].sum() == 0:
111166
logger.info("No bribes to process for StakeDAO")
112167
return
113168

114-
base_dir = Path(__file__).parent.parent
115169
campaign_manager = SafeContract(
116170
self.campaign_remote_manager_address,
117-
abi_file_path=f"{base_dir}/abi/stakedao_marketv2.json"
171+
abi_file_path=str(self.ABI_DIR / "stakedao_marketv2.json")
118172
)
119173

120174
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
152206

153207
aura_only = is_alliance or voting_override == "aura"
154208
bal_only = voting_override == "bal"
155-
addresses = [AURA_VEBAL_LOCKER] if aura_only or bal_only else []
209+
addresses = [self.AURA_VEBAL_LOCKER] if aura_only or bal_only else []
156210
is_whitelist = aura_only
157211

158212
campaign_params = (
@@ -168,12 +222,12 @@ def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> No
168222
is_whitelist,
169223
)
170224

171-
ccip_fee = self._calculate_ccip_fee(destination_chain_id, campaign_params)
225+
ccip_fee, additional_gas_limit = self._calculate_ccip_fee(destination_chain_id, destination_chain_name, campaign_params, vote_market_v2_address, builder.safe_address)
172226

173227
campaign_manager.createCampaign(
174228
campaign_params,
175229
destination_chain_id,
176-
0,
230+
additional_gas_limit,
177231
vote_market_v2_address,
178232
value=ccip_fee
179233
)

0 commit comments

Comments
 (0)