From f73aab01929f2e1bca873193a5fbfbc33208387f Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Thu, 26 Sep 2024 15:20:53 +0300 Subject: [PATCH 1/9] feat: add helper for transmitting emissions --- .../XChainLiquidityGaugeTransmitter.vy | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy diff --git a/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy b/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy new file mode 100644 index 0000000..1daa396 --- /dev/null +++ b/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy @@ -0,0 +1,304 @@ +# @version 0.4.0 +""" +@title XChainLiquidityGaugeTransmitter +@license MIT +@author Curve Finance +@notice Helper contract for transmitting new emissions to L2s. Non-ETH gas tokens are not supported yet. +@custom:version 0.0.1 +""" + + + +from ethereum.ercs import IERC20 + +interface Bridger: + def cost() -> uint256: view + +interface RootGauge: + def transmit_emissions(): nonpayable + def total_emissions() -> uint256: view + def last_period() -> uint256: view + def bridger() -> Bridger: view + def inflation_params() -> InflationParams: view + +interface GaugeController: + def checkpoint_gauge(addr: address): nonpayable + def n_gauge_types() -> int128: view + def gauge_types(_addr: address) -> int128: view + def points_weight(gauge_addr: address, time: uint256) -> Point: view # gauge_addr -> time -> Point +# def changes_weight: HashMap[address, HashMap[uint256, uint256]] # gauge_addr -> time -> slope + def time_weight(gauge_addr: address) -> uint256: view # gauge_addr -> last scheduled time (next week) + def points_sum(type_id: int128, time: uint256) -> Point: view # type_id -> time -> Point +# def changes_sum: HashMap[int128, HashMap[uint256, uint256]] # type_id -> time -> slope + def time_sum(type_id: uint256) -> uint256: view # type_id -> last scheduled time (next week) + def points_total(time: uint256) -> uint256: view # time -> total weight + def time_total() -> uint256: view # last scheduled time + def points_type_weight(type_id: int128, time: uint256) -> uint256: view # type_id -> time -> type weight + def time_type_weight(type_id: uint256) -> uint256: view # type_id -> last scheduled time (next week) + +interface Minter: + def minted(_user: address, _gauge: RootGauge) -> uint256: view + + +# Gauge controller replication +struct Point: + bias: uint256 + slope: uint256 + +# RootGauge replication +struct InflationParams: + rate: uint256 + finish_time: uint256 + +# Gas for bridgers +struct GasTopUp: + amount: uint256 + token: IERC20 # ETH_ADDRESS for raw ETH + receiver: address + + +CRV: public(constant(IERC20)) = IERC20(0xD533a949740bb3306d119CC777fa900bA034cd52) +ETH_ADDRESS: constant(address) = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE +MAX_LEN: constant(uint256) = 64 + +MINTER: public(constant(Minter)) = Minter(0xd061D61a4d941c39E5453435B6345Dc261C2fcE0) + +# Gauge related +GAUGE_CONTROLLER: public(constant(GaugeController)) = GaugeController(0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB) +WEEK: constant(uint256) = 604800 +YEAR: constant(uint256) = 86400 * 365 +RATE_DENOMINATOR: constant(uint256) = 10 ** 18 +RATE_REDUCTION_COEFFICIENT: constant(uint256) = 1189207115002721024 # 2 ** (1/4) * 1e18 +RATE_REDUCTION_TIME: constant(uint256) = YEAR + + +@internal +def _transmit(_gauge: RootGauge) -> uint256: + transmitted: uint256 = staticcall _gauge.total_emissions() - staticcall CRV.balanceOf(_gauge.address) + + extcall _gauge.transmit_emissions() + + return staticcall _gauge.total_emissions() - transmitted - staticcall CRV.balanceOf(_gauge.address) + + +@external +@payable +def transmit( + _min_amount: uint256, + _gauges: DynArray[RootGauge, MAX_LEN], + _gas_top_ups: DynArray[GasTopUp, MAX_LEN]=[], + _eth_refund: address=msg.sender, +) -> uint256: + """ + @notice Transmit emissions for xchain gauges + @param _min_amount Minimum amount + @param _gauges Gauges to transmit emissions for + @param _gas_top_ups Gas amount to send + @param _eth_refund Receiver of excess ETH (msg.sender by default) + @return Number of gauges surpassed `_min_amount` emissions + """ + self._top_up(_gas_top_ups if len(_gas_top_ups) > 0 else self._get_gas_top_ups(_gauges), _eth_refund) + surpassed: uint256 = 0 + for gauge: RootGauge in _gauges: + if self._transmit(gauge) >= _min_amount: + surpassed += 1 + return surpassed + + +@payable +@internal +def _top_up(top_ups: DynArray[GasTopUp, MAX_LEN], eth_refund: address): + eth_sent: uint256 = 0 + for top_up: GasTopUp in top_ups: + if top_up.amount != 0: + if top_up.token.address == ETH_ADDRESS: + send(top_up.receiver, top_up.amount) + eth_sent += top_up.amount + else: + # transfer coins beforehand + extcall top_up.token.transfer(top_up.receiver, top_up.amount) + if eth_refund != empty(address): + send(eth_refund, msg.value - eth_sent) + + +@external +@payable +def top_up_gas(_top_ups: DynArray[GasTopUp, MAX_LEN], _eth_refund: address=msg.sender): + """ + @notice Top up contracts for transmitting emissions + @param _top_ups Gas amount to send + @param _eth_refund Receiver of excess ETH (msg.sender by default) + """ + self._top_up(_top_ups, _eth_refund) + + +@view +@internal +def _get_gas_top_ups(gauges: DynArray[RootGauge, MAX_LEN]) -> DynArray[GasTopUp, MAX_LEN]: + top_ups: DynArray[GasTopUp, MAX_LEN] = [] + for gauge: RootGauge in gauges: + bridger: Bridger = staticcall gauge.bridger() + if bridger == empty(Bridger): + top_ups.append(empty(GasTopUp)) + else: + bal: uint256 = gauge.address.balance + cost: uint256 = staticcall bridger.cost() + print("Balance ", bal) + print("Cost ", cost) + top_ups.append( + GasTopUp( + amount = max(staticcall bridger.cost(), bal) - bal, + token = IERC20(ETH_ADDRESS), + receiver = gauge.address, + ) + ) + return top_ups + + +@view +@external +def get_gas_top_ups(_gauges: DynArray[RootGauge, MAX_LEN]) -> DynArray[GasTopUp, MAX_LEN]: + """ + @notice Get amounts of gas for bridging. Non ETH gas not supported. + @param _gauges Gauges intended for transmission + """ + return self._get_gas_top_ups(_gauges) + + +### GAUGE_CONTROLLER replication +### Can not follow fully bc of private variables, +### should work in most cases + + +@view +@internal +def _get_weight(gauge: RootGauge, time: uint256) -> uint256: + t: uint256 = staticcall GAUGE_CONTROLLER.time_weight(gauge.address) + if t > 0: + pt: Point = staticcall GAUGE_CONTROLLER.points_weight(gauge.address, t) + for i: uint256 in range(500): + if t >= time: + break + t += WEEK + d_bias: uint256 = pt.slope * WEEK + if pt.bias > d_bias: + pt.bias -= d_bias + # d_slope: uint256 = staticcall GAUGE_CONTROLLER.changes_weight(gauge_addr, t) + # pt.slope -= d_slope + else: + pt.bias = 0 + pt.slope = 0 + return pt.bias + else: + return 0 + + +@view +@internal +def _get_sum(gauge_type: int128, time: uint256) -> uint256: + t: uint256 = min(staticcall GAUGE_CONTROLLER.time_sum(convert(gauge_type, uint256)), time) + if t > 0: + pt: Point = staticcall GAUGE_CONTROLLER.points_sum(gauge_type, t) + for i: uint256 in range(500): + if t >= time: + break + t += WEEK + d_bias: uint256 = pt.slope * WEEK + if pt.bias > d_bias: + pt.bias -= d_bias + # d_slope: uint256 = staticcall GAUGE_CONTROLLER.changes_sum(gauge_type, t) + # pt.slope -= d_slope + else: + pt.bias = 0 + pt.slope = 0 + return pt.bias + else: + return 0 + + +@view +@internal +def _get_type_weight(gauge_type: int128, time: uint256) -> uint256: + t: uint256 = min(staticcall GAUGE_CONTROLLER.time_type_weight(convert(gauge_type, uint256)), time) + if t > 0: + return staticcall GAUGE_CONTROLLER.points_type_weight(gauge_type, t) + else: + return 0 + + +@view +@internal +def _get_total(gauge: RootGauge, time: uint256) -> uint256: + t: uint256 = min(staticcall GAUGE_CONTROLLER.time_total(), time) + _n_gauge_types: int128 = staticcall GAUGE_CONTROLLER.n_gauge_types() + if t >= time + WEEK: + return staticcall GAUGE_CONTROLLER.points_total(time) + + pt: uint256 = 0 + for gauge_type: int128 in range(100): + type_sum: uint256 = self._get_sum(gauge_type, time) + type_weight: uint256 = self._get_type_weight(gauge_type, time) + pt += type_sum * type_weight + return pt + + + +@view +@internal +def _gauge_relative_weight(gauge: RootGauge, time: uint256) -> uint256: + t: uint256 = time // WEEK * WEEK + _total_weight: uint256 = self._get_total(gauge, t) + + if _total_weight > 0: + gauge_type: int128 = staticcall GAUGE_CONTROLLER.gauge_types(gauge.address) + _type_weight: uint256 = self._get_type_weight(gauge_type, t) + _gauge_weight: uint256 = self._get_weight(gauge, t) + return 10 ** 18 * _type_weight * _gauge_weight // _total_weight + + else: + return 0 + + +@view +@internal +def _to_mint(gauge: RootGauge, ts: uint256) -> uint256: + last_period: uint256 = staticcall gauge.last_period() + current_period: uint256 = ts // WEEK + + params: InflationParams = staticcall gauge.inflation_params() + emissions: uint256 = staticcall gauge.total_emissions() + + if last_period < current_period: + for i: uint256 in range(last_period, current_period, bound=256): + period_time: uint256 = i * WEEK + weight: uint256 = self._gauge_relative_weight(gauge, period_time) + + if period_time <= params.finish_time and params.finish_time < period_time + WEEK: + emissions += weight * params.rate * (params.finish_time - period_time) // 10 ** 18 + params.rate = params.rate * RATE_DENOMINATOR // RATE_REDUCTION_COEFFICIENT + emissions += weight * params.rate * (period_time + WEEK - params.finish_time) // 10 ** 18 + params.finish_time += RATE_REDUCTION_TIME + else: + emissions += weight * params.rate * WEEK // 10 ** 18 + + return emissions - staticcall MINTER.minted(gauge.address, gauge) + + +@view +@external +def calculate_emissions( + _gauges: DynArray[RootGauge, MAX_LEN], _ts: uint256 = block.timestamp, +) -> DynArray[uint256, MAX_LEN]: + """ + @notice Calculate amounts of CRV being transmitted at `_ts`. + Gas-guzzling function, considered for off-chain use. + Also not precise, better to simulate txs beforehand. + @dev Replicated logic from GaugeController, but not precise because some variables are private. + @param _gauges List of gauge addresses + @param _ts Timestamp at which to calculate + @return Amounts of CRV to be transmitted at `_ts` + """ + emissions: DynArray[uint256, MAX_LEN] = [] + for gauge: RootGauge in _gauges: + emissions.append(staticcall CRV.balanceOf(gauge.address) + self._to_mint(gauge, _ts)) + return emissions From 40b2ba0b014322cc5ea74ae02fc268f09fb23d9b Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Thu, 26 Sep 2024 15:40:15 +0300 Subject: [PATCH 2/9] fix: get_weight --- contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy b/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy index 1daa396..2c91e54 100644 --- a/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy +++ b/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy @@ -173,7 +173,7 @@ def get_gas_top_ups(_gauges: DynArray[RootGauge, MAX_LEN]) -> DynArray[GasTopUp, @view @internal def _get_weight(gauge: RootGauge, time: uint256) -> uint256: - t: uint256 = staticcall GAUGE_CONTROLLER.time_weight(gauge.address) + t: uint256 = min(staticcall GAUGE_CONTROLLER.time_weight(gauge.address), time) if t > 0: pt: Point = staticcall GAUGE_CONTROLLER.points_weight(gauge.address, t) for i: uint256 in range(500): From 2dd4dece8826038b1ab6ba74865fcf2c0902792d Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Sun, 29 Sep 2024 19:00:29 +0300 Subject: [PATCH 3/9] fix: call gauge via factory --- .../hooks/ethereum/XChainLiquidityGaugeTransmitter.vy | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy b/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy index 2c91e54..ebb7ee0 100644 --- a/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy +++ b/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy @@ -14,7 +14,11 @@ from ethereum.ercs import IERC20 interface Bridger: def cost() -> uint256: view +interface GaugeFactory: + def transmit_emissions(_gauge: address): nonpayable + interface RootGauge: + def factory() -> GaugeFactory: view def transmit_emissions(): nonpayable def total_emissions() -> uint256: view def last_period() -> uint256: view @@ -76,7 +80,8 @@ RATE_REDUCTION_TIME: constant(uint256) = YEAR def _transmit(_gauge: RootGauge) -> uint256: transmitted: uint256 = staticcall _gauge.total_emissions() - staticcall CRV.balanceOf(_gauge.address) - extcall _gauge.transmit_emissions() + factory: GaugeFactory = staticcall _gauge.factory() + extcall factory.transmit_emissions(_gauge.address) return staticcall _gauge.total_emissions() - transmitted - staticcall CRV.balanceOf(_gauge.address) From 7f0d8437ac71903f140480d68e2ef9a6963c2469 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Sun, 29 Sep 2024 19:05:34 +0300 Subject: [PATCH 4/9] chore: add contract eligibility check --- contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy b/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy index ebb7ee0..c4da96d 100644 --- a/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy +++ b/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy @@ -105,6 +105,8 @@ def transmit( self._top_up(_gas_top_ups if len(_gas_top_ups) > 0 else self._get_gas_top_ups(_gauges), _eth_refund) surpassed: uint256 = 0 for gauge: RootGauge in _gauges: + # Check that address is actually approved contract + type: int128 = staticcall GAUGE_CONTROLLER.gauge_types(gauge.address) if self._transmit(gauge) >= _min_amount: surpassed += 1 return surpassed @@ -148,8 +150,6 @@ def _get_gas_top_ups(gauges: DynArray[RootGauge, MAX_LEN]) -> DynArray[GasTopUp, else: bal: uint256 = gauge.address.balance cost: uint256 = staticcall bridger.cost() - print("Balance ", bal) - print("Cost ", cost) top_ups.append( GasTopUp( amount = max(staticcall bridger.cost(), bal) - bal, From 644e66e2769224fb3e9897585e14de3f4f0eff29 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Tue, 1 Oct 2024 11:18:12 +0300 Subject: [PATCH 5/9] chore: interpolate unknown variables --- .../XChainLiquidityGaugeTransmitter.vy | 65 ++++++++++++------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy b/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy index c4da96d..8f17bab 100644 --- a/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy +++ b/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy @@ -7,6 +7,7 @@ @custom:version 0.0.1 """ +version: public(constant(String[8])) = "0.0.1" from ethereum.ercs import IERC20 @@ -181,18 +182,25 @@ def _get_weight(gauge: RootGauge, time: uint256) -> uint256: t: uint256 = min(staticcall GAUGE_CONTROLLER.time_weight(gauge.address), time) if t > 0: pt: Point = staticcall GAUGE_CONTROLLER.points_weight(gauge.address, t) - for i: uint256 in range(500): - if t >= time: - break - t += WEEK - d_bias: uint256 = pt.slope * WEEK - if pt.bias > d_bias: - pt.bias -= d_bias - # d_slope: uint256 = staticcall GAUGE_CONTROLLER.changes_weight(gauge_addr, t) - # pt.slope -= d_slope - else: - pt.bias = 0 - pt.slope = 0 + if t < time: + d_slope: uint256 = 0 # Will hallucinate up + # Try interpolation if at least 2 points known + prev_pt: Point = staticcall GAUGE_CONTROLLER.points_weight(gauge.address, t - WEEK) + if prev_pt.slope > pt.slope: + d_slope = prev_pt.slope - pt.slope + + for i: uint256 in range(500): + if t >= time: + break + t += WEEK + d_bias: uint256 = pt.slope * WEEK + if pt.bias > d_bias: + pt.bias -= d_bias + # d_slope: uint256 = staticcall GAUGE_CONTROLLER.points_weight(gauge_addr, t - WEEK) + pt.slope -= d_slope + else: + pt.bias = 0 + pt.slope = 0 return pt.bias else: return 0 @@ -204,18 +212,25 @@ def _get_sum(gauge_type: int128, time: uint256) -> uint256: t: uint256 = min(staticcall GAUGE_CONTROLLER.time_sum(convert(gauge_type, uint256)), time) if t > 0: pt: Point = staticcall GAUGE_CONTROLLER.points_sum(gauge_type, t) - for i: uint256 in range(500): - if t >= time: - break - t += WEEK - d_bias: uint256 = pt.slope * WEEK - if pt.bias > d_bias: - pt.bias -= d_bias - # d_slope: uint256 = staticcall GAUGE_CONTROLLER.changes_sum(gauge_type, t) - # pt.slope -= d_slope - else: - pt.bias = 0 - pt.slope = 0 + if t < time: + d_slope: uint256 = 0 # Will hallucinate up + # Try interpolation if at least 2 points known + prev_pt: Point = staticcall GAUGE_CONTROLLER.points_sum(gauge_type, t - WEEK) + if prev_pt.slope > pt.slope: + d_slope = prev_pt.slope - pt.slope + + for i: uint256 in range(500): + if t >= time: + break + t += WEEK + d_bias: uint256 = pt.slope * WEEK + if pt.bias > d_bias: + pt.bias -= d_bias + # d_slope: uint256 = staticcall GAUGE_CONTROLLER.changes_sum(gauge_type, t) + pt.slope -= d_slope + else: + pt.bias = 0 + pt.slope = 0 return pt.bias else: return 0 @@ -297,7 +312,7 @@ def calculate_emissions( """ @notice Calculate amounts of CRV being transmitted at `_ts`. Gas-guzzling function, considered for off-chain use. - Also not precise, better to simulate txs beforehand. + Might hallucinate, better to simulate txs beforehand. @dev Replicated logic from GaugeController, but not precise because some variables are private. @param _gauges List of gauge addresses @param _ts Timestamp at which to calculate From 0f3eec8c69a079092084c1349fb75132eae2b5d2 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Tue, 1 Oct 2024 11:23:01 +0300 Subject: [PATCH 6/9] chore: full balance check so no owner for recover needed --- contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy b/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy index 8f17bab..c4f1924 100644 --- a/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy +++ b/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy @@ -126,7 +126,7 @@ def _top_up(top_ups: DynArray[GasTopUp, MAX_LEN], eth_refund: address): # transfer coins beforehand extcall top_up.token.transfer(top_up.receiver, top_up.amount) if eth_refund != empty(address): - send(eth_refund, msg.value - eth_sent) + send(eth_refund, self.balance - eth_sent) @external From 9d3a7791d48684ec394b6babacdc79ea827c7631 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Wed, 9 Oct 2024 12:25:45 +0300 Subject: [PATCH 7/9] feat: add fee splitter to forward sample --- fee_keeper/sample_forward.py | 37 +++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/fee_keeper/sample_forward.py b/fee_keeper/sample_forward.py index a4d3382..7db1b0f 100644 --- a/fee_keeper/sample_forward.py +++ b/fee_keeper/sample_forward.py @@ -31,6 +31,8 @@ }[chain] EMPTY_HOOK_INPUT = (0, 0, b"") +COLLECT_FEES_CALL = bytes.fromhex("1e0cfcef") + web3 = Web3( provider=Web3.HTTPProvider( RPC[chain], @@ -53,7 +55,7 @@ def account_load_pkey(fname): "https://rpc.beaverbuild.org/", "https://rpc.titanbuilder.xyz/", "https://rsync-builder.xyz", - "https://relay.flashbots.net", + # "https://relay.flashbots.net", ] class DataFetcher: @@ -124,9 +126,13 @@ async def get_amounts(self): def forward(prev_tx, calls): multicall = web3.eth.contract("0xcA11bde05977b3631167028862bE2a173976CA11", abi=[{"inputs": [{"components": [{"internalType": "address", "name": "target", "type": "address"},{"internalType": "bool", "name": "allowFailure", "type": "bool"},{"internalType": "bytes", "name": "callData", "type": "bytes"}], "internalType": "struct Multicall3.Call3[]","name": "calls","type": "tuple[]"}],"name": "aggregate3", "outputs": [{"components": [{"internalType": "bool", "name": "success", "type": "bool"},{"internalType": "bytes", "name": "returnData", "type": "bytes"}],"internalType": "struct Multicall3.Result[]", "name": "returnData", "type": "tuple[]"}],"stateMutability": "payable","type": "function"}, ]) fee_collector = web3.eth.contract(FEE_COLLECTOR, abi=[{"stateMutability": "payable", "type": "function", "name": "forward", "inputs": [{"name": "_hook_inputs", "type": "tuple[]","components": [{"name": "hook_id", "type": "uint8"}, {"name": "value", "type": "uint256"},{"name": "data", "type": "bytes"}]}], "outputs": [{"name": "", "type": "uint256"}]},{"stateMutability": "payable", "type": "function", "name": "forward", "inputs": [{"name": "_hook_inputs", "type": "tuple[]","components": [{"name": "hook_id", "type": "uint8"}, {"name": "value", "type": "uint256"},{"name": "data", "type": "bytes"}]}, {"name": "_receiver", "type": "address"}],"outputs": [{"name": "", "type": "uint256"}]}, ], ) + fee_splitter = web3.eth.contract("0x2dFd89449faff8a532790667baB21cF733C064f2", abi=[{"anonymous":False,"inputs":[],"name":"SetReceivers","type":"event"},{"anonymous":False,"inputs":[],"name":"LivenessProtectionTriggered","type":"event"},{"anonymous":False,"inputs":[{"indexed":True,"name":"receiver","type":"address"},{"indexed":False,"name":"weight","type":"uint256"}],"name":"FeeDispatched","type":"event"},{"anonymous":False,"inputs":[{"indexed":True,"name":"previous_owner","type":"address"},{"indexed":True,"name":"new_owner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"inputs":[{"name":"new_owner","type":"address"}],"name":"transfer_ownership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounce_ownership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"update_controllers","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"n_controllers","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"}],"name":"allowed_controllers","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"}],"name":"controllers","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"dispatch_fees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"controllers","type":"address[]"}],"name":"dispatch_fees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"name":"addr","type":"address"},{"name":"weight","type":"uint256"}],"name":"receivers","type":"tuple[]"}],"name":"set_receivers","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"excess_receiver","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"n_receivers","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"version","outputs":[{"name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"}],"name":"receivers","outputs":[{"components":[{"name":"addr","type":"address"},{"name":"weight","type":"uint256"}],"name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_crvusd","type":"address"},{"name":"_factory","type":"address"},{"components":[{"name":"addr","type":"address"},{"name":"weight","type":"uint256"}],"name":"receivers","type":"tuple[]"},{"name":"owner","type":"address"}],"outputs":[],"stateMutability":"nonpayable","type":"constructor"}]) nonce = web3.eth.get_transaction_count(wallet_address) + controllers = [addr for addr, _, method in calls if method == COLLECT_FEES_CALL] + calls = [call for call in calls if call[2] != COLLECT_FEES_CALL] calls += [ + (fee_splitter.address, False, fee_splitter.encodeABI("dispatch_fees", (controllers,))), (fee_collector.address, False, fee_collector.encodeABI("forward", ([EMPTY_HOOK_INPUT], "0xcb78EA4Bc3c545EB48dDC9b8302Fa9B03d1B1B61"))), ] max_fee = 20 * 10 ** 9 # even 10 GWEI should be enough for Wednesday morning @@ -138,18 +144,25 @@ def forward(prev_tx, calls): "from": wallet_address, "nonce": nonce, "maxFeePerGas": max_fee, "maxPriorityFeePerGas": max_priority, })) - print(calls) + print(prev_tx, calls, controllers) txs.append(multicall.functions.aggregate3(calls).build_transaction({ "from": wallet_address, "nonce": nonce + (1 if prev_tx else 0), "maxFeePerGas": max_fee, "maxPriorityFeePerGas": max_priority, })) iters = 0 - while web3.eth.get_transaction_count(wallet_address) <= nonce and iters < 2: + while web3.eth.get_transaction_count(wallet_address) <= nonce and iters < 3: try: for tx in txs: gas_estimate = web3.eth.estimate_gas(tx) - tx["gas"] = int(2 * gas_estimate) + assert gas_estimate < 30_000_000, "Block gas exceeded" + tx["gas"] = max( + min(int(2.0 * gas_estimate), 20 * 10 ** 6), + min(int(1.5 * gas_estimate), 25 * 10 ** 6), + min(int(1.1 * gas_estimate), 30 * 10 ** 6), + ) + if tx["gas"] > 20 * 10 ** 6: + print(f"WARNING: huge gas = {tx['gas']}") except Exception as e: print("Could not estimate gas", repr(e)) return @@ -166,7 +179,7 @@ def forward(prev_tx, calls): { "version": "v0.1", "inclusion": {"block": str(hex(block)), "maxBlock": str(hex(block))}, - "body": [{"tx": tx.rawTransaction.hex(), "canRevert": True} for tx in signed_txs], + "body": [{"tx": tx.raw_transaction.hex(), "canRevert": True} for tx in signed_txs], } ] }) @@ -177,7 +190,7 @@ def forward(prev_tx, calls): "method": "eth_sendBundle", "params": [ { - "txs": [tx.rawTransaction.hex() for tx in signed_txs], + "txs": [tx.raw_transaction.hex() for tx in signed_txs], "blockNumber": str(hex(block)), } ] @@ -244,7 +257,7 @@ async def run(): for controller, amount in controllers.items(): try: if amount >= safe_threshold: # TODO check balance of controller in case of rug_debt_ceiling - calls.append((controller, False, bytes.fromhex("1e0cfcef"))) + calls.append((controller, False, COLLECT_FEES_CALL)) cnt += 1 ; total += amount except Exception as e: print(f"{controller} admin_fees() {repr(e)}") @@ -276,4 +289,14 @@ async def run(): if __name__ == "__main__": + # forward( + # prev_tx=None, + # calls=[ + # ("0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", False, COLLECT_FEES_CALL), # ETH + # ("0x4e59541306910aD6dC1daC0AC9dFB29bD9F15c67", False, COLLECT_FEES_CALL), # wBTC + # ("0x100dAa78fC509Db39Ef7D04DE0c1ABD299f4C6CE", False, COLLECT_FEES_CALL), # wstETH + # ("0x1C91da0223c763d2e0173243eAdaA0A2ea47E704", False, COLLECT_FEES_CALL), # tBTC + # ("0xEC0820EfafC41D8943EE8dE495fC9Ba8495B15cf", False, COLLECT_FEES_CALL), # sfrxETH v2 + # ] + # ) asyncio.run(run()) From a2ed95d43ad471425095cad9afa712a9376696b7 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Mon, 27 Oct 2025 13:39:31 +0300 Subject: [PATCH 8/9] feat: simple script to collect --- fee_keeper/fast_collect.py | 144 +++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 fee_keeper/fast_collect.py diff --git a/fee_keeper/fast_collect.py b/fee_keeper/fast_collect.py new file mode 100644 index 0000000..236b36d --- /dev/null +++ b/fee_keeper/fast_collect.py @@ -0,0 +1,144 @@ +""" +Script template to one-time collect of fees found manually through some special route. +Follow "# ALTER" lines to fill all needed data. +""" +import time +import requests +import os +from dotenv import load_dotenv + +from web3 import Web3 +from eth_account import Account + + +chain = "etherum" # ALTER: chain +FEE_COLLECTOR = { + "ethereum": "0xa2Bcd1a4Efbd04B63cd03f5aFf2561106ebCCE00", + "xdai": "0xBb7404F9965487a9DdE721B3A5F0F3CcfA9aa4C5", +}["ethereum"] +MULTICALL = "0xcA11bde05977b3631167028862bE2a173976CA11" +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + +BUILDERS = [ + "https://rpc.beaverbuild.org/", + "https://rpc.titanbuilder.xyz/", + "https://rsync-builder.xyz", + "https://relay.flashbots.net", +] + +web3 = Web3( + provider=Web3.HTTPProvider( + "http://localhost:8545", # ALTER: Chain RPC + ), +) +if os.path.exists(".env"): + load_dotenv() +account = Account.from_key(os.getenv("ACCOUNT_PK")) # ALTER: load private key + + +def route_logic(): + # ALTER: add logic to collect coins, example with frxeth pool below + prev_owner = web3.eth.contract("0x173428bD726dBAb910de28527A7b7DDf2AC0444f", abi=[{'stateMutability': 'nonpayable', 'type': 'constructor', 'inputs': [], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'commit_transfer_ownership', 'inputs': [{'name': '_new_owner', 'type': 'address'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'apply_transfer_ownership', 'inputs': [], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'revert_transfer_ownership', 'inputs': [], 'outputs': []}, {'stateMutability': 'view', 'type': 'function', 'name': 'admin', 'inputs': [], 'outputs': [{'name': '', 'type': 'address'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'pool', 'inputs': [], 'outputs': [{'name': '', 'type': 'address'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'future_pool_owner', 'inputs': [], 'outputs': [{'name': '', 'type': 'address'}]}]) + frxeth_owner = web3.eth.contract("0xA78f1256C2e00bE91d26C4504FA5E9A6dEDeB306", abi=[{'stateMutability': 'payable', 'type': 'fallback'}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'commit_transfer_ownership', 'inputs': [{'name': '_new_owner', 'type': 'address'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'apply_transfer_ownership', 'inputs': [], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'revert_transfer_ownership', 'inputs': [], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'withdraw_admin_fees', 'inputs': [], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'set_fee_receiver', 'inputs': [{'name': '_fee_receiver', 'type': 'address'}], 'outputs': []}, {'stateMutability': 'view', 'type': 'function', 'name': 'admin', 'inputs': [], 'outputs': [{'name': '', 'type': 'address'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'pool', 'inputs': [], 'outputs': [{'name': '', 'type': 'address'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'future_pool_owner', 'inputs': [], 'outputs': [{'name': '', 'type': 'address'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'fee_receiver', 'inputs': [], 'outputs': [{'name': '', 'type': 'address'}]}, {'stateMutability': 'nonpayable', 'type': 'constructor', 'inputs': [], 'outputs': []}]) + return [ + # (target, allowFailure, callData) + (prev_owner.address, True, prev_owner.encodeABI(fn_name="apply_transfer_ownership", args=[])), + (frxeth_owner.address, False, frxeth_owner.encodeABI(fn_name="withdraw_admin_fees", args=[])), + ] + + +def fee_collector_collect(): + fee_collector = web3.eth.contract( + FEE_COLLECTOR, + abi=[ + {"stateMutability":"nonpayable", "type": "function", "name": "withdraw_many", "inputs": [{"name": "_pools", "type": "address[]"}], "outputs": []}, + {"stateMutability":"nonpayable","type":"function","name":"collect","inputs":[{"name":"_coins","type":"address[]"},{"name":"_receiver","type":"address"}],"outputs":[]}, + ], + ) + + return [ + # (target, allowFailure, callData) + ( + fee_collector.address, + False, + fee_collector.encodeABI( + fn_name="collect", + args=[ + [ETH_ADDRESS, "0x5E8422345238F34275888049021821E8E08CAa1f"], # ALTER: List of coins, that FeeCollector will receive in result of route_logic + "0xcb78EA4Bc3c545EB48dDC9b8302Fa9B03d1B1B61", # ALTER: Address to receive fees from this operation + ] + ), + ) + ] + + +def build_multicall_transaction(calls): + multicall = web3.eth.contract(MULTICALL, abi=[{'inputs': [{'components': [{'internalType': 'address', 'name': 'target', 'type': 'address'}, {'internalType': 'bytes', 'name': 'callData', 'type': 'bytes'}], 'internalType': 'struct Multicall3.Call[]', 'name': 'calls', 'type': 'tuple[]'}], 'name': 'aggregate', 'outputs': [{'internalType': 'uint256', 'name': 'blockNumber', 'type': 'uint256'}, {'internalType': 'bytes[]', 'name': 'returnData', 'type': 'bytes[]'}], 'stateMutability': 'payable', 'type': 'function'}, {'inputs': [{'components': [{'internalType': 'address', 'name': 'target', 'type': 'address'}, {'internalType': 'bool', 'name': 'allowFailure', 'type': 'bool'}, {'internalType': 'bytes', 'name': 'callData', 'type': 'bytes'}], 'internalType': 'struct Multicall3.Call3[]', 'name': 'calls', 'type': 'tuple[]'}], 'name': 'aggregate3', 'outputs': [{'components': [{'internalType': 'bool', 'name': 'success', 'type': 'bool'}, {'internalType': 'bytes', 'name': 'returnData', 'type': 'bytes'}], 'internalType': 'struct Multicall3.Result[]', 'name': 'returnData', 'type': 'tuple[]'}], 'stateMutability': 'payable', 'type': 'function'}, {'inputs': [{'components': [{'internalType': 'address', 'name': 'target', 'type': 'address'}, {'internalType': 'bool', 'name': 'allowFailure', 'type': 'bool'}, {'internalType': 'uint256', 'name': 'value', 'type': 'uint256'}, {'internalType': 'bytes', 'name': 'callData', 'type': 'bytes'}], 'internalType': 'struct Multicall3.Call3Value[]', 'name': 'calls', 'type': 'tuple[]'}], 'name': 'aggregate3Value', 'outputs': [{'components': [{'internalType': 'bool', 'name': 'success', 'type': 'bool'}, {'internalType': 'bytes', 'name': 'returnData', 'type': 'bytes'}], 'internalType': 'struct Multicall3.Result[]', 'name': 'returnData', 'type': 'tuple[]'}], 'stateMutability': 'payable', 'type': 'function'}, {'inputs': [{'components': [{'internalType': 'address', 'name': 'target', 'type': 'address'}, {'internalType': 'bytes', 'name': 'callData', 'type': 'bytes'}], 'internalType': 'struct Multicall3.Call[]', 'name': 'calls', 'type': 'tuple[]'}], 'name': 'blockAndAggregate', 'outputs': [{'internalType': 'uint256', 'name': 'blockNumber', 'type': 'uint256'}, {'internalType': 'bytes32', 'name': 'blockHash', 'type': 'bytes32'}, {'components': [{'internalType': 'bool', 'name': 'success', 'type': 'bool'}, {'internalType': 'bytes', 'name': 'returnData', 'type': 'bytes'}], 'internalType': 'struct Multicall3.Result[]', 'name': 'returnData', 'type': 'tuple[]'}], 'stateMutability': 'payable', 'type': 'function'}, {'inputs': [], 'name': 'getBasefee', 'outputs': [{'internalType': 'uint256', 'name': 'basefee', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': 'blockNumber', 'type': 'uint256'}], 'name': 'getBlockHash', 'outputs': [{'internalType': 'bytes32', 'name': 'blockHash', 'type': 'bytes32'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'getBlockNumber', 'outputs': [{'internalType': 'uint256', 'name': 'blockNumber', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'getChainId', 'outputs': [{'internalType': 'uint256', 'name': 'chainid', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'getCurrentBlockCoinbase', 'outputs': [{'internalType': 'address', 'name': 'coinbase', 'type': 'address'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'getCurrentBlockDifficulty', 'outputs': [{'internalType': 'uint256', 'name': 'difficulty', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'getCurrentBlockGasLimit', 'outputs': [{'internalType': 'uint256', 'name': 'gaslimit', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'getCurrentBlockTimestamp', 'outputs': [{'internalType': 'uint256', 'name': 'timestamp', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'address', 'name': 'addr', 'type': 'address'}], 'name': 'getEthBalance', 'outputs': [{'internalType': 'uint256', 'name': 'balance', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'getLastBlockHash', 'outputs': [{'internalType': 'bytes32', 'name': 'blockHash', 'type': 'bytes32'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'bool', 'name': 'requireSuccess', 'type': 'bool'}, {'components': [{'internalType': 'address', 'name': 'target', 'type': 'address'}, {'internalType': 'bytes', 'name': 'callData', 'type': 'bytes'}], 'internalType': 'struct Multicall3.Call[]', 'name': 'calls', 'type': 'tuple[]'}], 'name': 'tryAggregate', 'outputs': [{'components': [{'internalType': 'bool', 'name': 'success', 'type': 'bool'}, {'internalType': 'bytes', 'name': 'returnData', 'type': 'bytes'}], 'internalType': 'struct Multicall3.Result[]', 'name': 'returnData', 'type': 'tuple[]'}], 'stateMutability': 'payable', 'type': 'function'}, {'inputs': [{'internalType': 'bool', 'name': 'requireSuccess', 'type': 'bool'}, {'components': [{'internalType': 'address', 'name': 'target', 'type': 'address'}, {'internalType': 'bytes', 'name': 'callData', 'type': 'bytes'}], 'internalType': 'struct Multicall3.Call[]', 'name': 'calls', 'type': 'tuple[]'}], 'name': 'tryBlockAndAggregate', 'outputs': [{'internalType': 'uint256', 'name': 'blockNumber', 'type': 'uint256'}, {'internalType': 'bytes32', 'name': 'blockHash', 'type': 'bytes32'}, {'components': [{'internalType': 'bool', 'name': 'success', 'type': 'bool'}, {'internalType': 'bytes', 'name': 'returnData', 'type': 'bytes'}], 'internalType': 'struct Multicall3.Result[]', 'name': 'returnData', 'type': 'tuple[]'}], 'stateMutability': 'payable', 'type': 'function'}]) + nonce = web3.eth.get_transaction_count(account=account.address) + # print(f"multicall calldata: {multicall.encodeABI(fn_name='aggregate3', args=[calls])}") + tx = multicall.functions.aggregate3(calls).build_transaction( + { + "from": account.address, + "nonce": nonce, + "maxFeePerGas": 5 * 10 ** 9, + "maxPriorityFeePerGas": 1 * 10 ** 9, + } + ) + signed_tx = web3.eth.account.sign_transaction(tx, private_key=account.private_key) + return signed_tx + + +def send_transaction(signed_tx): + wallet_address = signed_tx["from"] + nonce = signed_tx["nonce"] + iters = 5 # ALTER: number of iterations to try + while web3.eth.get_transaction_count(wallet_address) < nonce and iters > 0: + block = web3.eth.get_block_number() + 1 + print(f"Trying block: {block}") + for builder in BUILDERS: + if "flashbots" in builder: + r = requests.post(builder, json={ + "jsonrpc": "2.0", + "id": 1, + "method": "eth_sendBundle", + "params": [ + { + "version": "v0.1", + "inclusion": {"block": str(hex(block)), "maxBlock": str(hex(block))}, + "body": [{"tx": signed_tx.rawTransaction.hex(), "canRevert": True}], + } + ] + }) + else: + r = requests.post(builder, json={ + "jsonrpc": "2.0", + "id": 1, + "method": "eth_sendBundle", + "params": [ + { + "txs": [signed_tx.rawTransaction.hex()], + "blockNumber": str(hex(block)), + } + ] + }) + print(builder, r.json()) + iters -= 1 + time.sleep(6) # wait some time between blocks + + +if __name__ == '__main__': + # construct calldata + calls = route_logic() + fee_collector_collect() + print(calls) + print("Press Enter to proceed", end='') ; input() + + signed_tx = build_multicall_transaction(calls) + wallet_address = signed_tx["from"] + nonce = signed_tx["nonce"] + print(f"Signed transaction from {wallet_address} with nonce {nonce}") + + # Send transaction to private builders + send_transaction(signed_tx) + if web3.eth.get_transaction_count(wallet_address) > nonce: + print("Go check ur wallet, I dit sth for ya ^&^") From 68185b1245012fe8650ca66f547c22c2fcdf6bfc Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Sat, 22 Nov 2025 00:26:39 +0300 Subject: [PATCH 9/9] feat: add off-chain call to fetch all active gauges --- .../XChainLiquidityGaugeTransmitter.vy | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy b/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy index c4f1924..b1f6f6c 100644 --- a/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy +++ b/contracts/hooks/ethereum/XChainLiquidityGaugeTransmitter.vy @@ -1,4 +1,4 @@ -# @version 0.4.0 +# pragma version 0.4.3 """ @title XChainLiquidityGaugeTransmitter @license MIT @@ -17,6 +17,8 @@ interface Bridger: interface GaugeFactory: def transmit_emissions(_gauge: address): nonpayable + def get_gauge_count(_chain_id: uint256) -> uint256: view + def get_gauge(_chain_id: uint256, _idx: uint256) -> address: view interface RootGauge: def factory() -> GaugeFactory: view @@ -25,6 +27,7 @@ interface RootGauge: def last_period() -> uint256: view def bridger() -> Bridger: view def inflation_params() -> InflationParams: view + def is_killed() -> bool: view interface GaugeController: def checkpoint_gauge(addr: address): nonpayable @@ -61,10 +64,16 @@ struct GasTopUp: token: IERC20 # ETH_ADDRESS for raw ETH receiver: address +struct GaugeInfo: + gauge: RootGauge + chain_id: uint256 + CRV: public(constant(IERC20)) = IERC20(0xD533a949740bb3306d119CC777fa900bA034cd52) ETH_ADDRESS: constant(address) = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE -MAX_LEN: constant(uint256) = 64 +MAX_LEN: constant(uint256) = 128 +MAX_GAUGES_PER_CHAIN: constant(uint256) = 1024 # Used in get_active_gauges view function +MAX_GAUGES: constant(uint256) = 1024 # Used in get_active_gauges view function MINTER: public(constant(Minter)) = Minter(0xd061D61a4d941c39E5453435B6345Dc261C2fcE0) @@ -171,6 +180,34 @@ def get_gas_top_ups(_gauges: DynArray[RootGauge, MAX_LEN]) -> DynArray[GasTopUp, return self._get_gas_top_ups(_gauges) +@view +@external +def get_active_gauges(_factory: GaugeFactory, _chain_ids: DynArray[uint256, MAX_LEN]) -> DynArray[GaugeInfo, MAX_GAUGES]: + """ + @notice Get active xchain gauges. + Gas-guzzling function, considered for off-chain use. + @param _factory Root gauge factory address + @param _chain_ids Chain IDs of networks to check gauges for + """ + gauges: DynArray[GaugeInfo, MAX_GAUGES] = empty(DynArray[GaugeInfo, MAX_GAUGES]) + for chain_id: uint256 in _chain_ids: + gauge_count: uint256 = staticcall _factory.get_gauge_count(chain_id) + for idx: uint256 in range(gauge_count, bound=MAX_GAUGES_PER_CHAIN): + gauge: RootGauge = RootGauge(staticcall _factory.get_gauge(chain_id, idx)) + success: bool = False + response: Bytes[32] = b"" + success, response = raw_call( + GAUGE_CONTROLLER.address, + abi_encode(gauge.address, method_id=method_id("gauge_types(address)")), + max_outsize=32, + is_static_call=True, + revert_on_failure=False, + ) + if success and not (staticcall gauge.is_killed()): + gauges.append(GaugeInfo(gauge=gauge, chain_id=chain_id)) + + return gauges + ### GAUGE_CONTROLLER replication ### Can not follow fully bc of private variables, ### should work in most cases