feat(oft): consolidated per-chain deploy scripts for listing infra + oracle peers#141
Open
jtfirek wants to merge 29 commits into
Open
feat(oft): consolidated per-chain deploy scripts for listing infra + oracle peers#141jtfirek wants to merge 29 commits into
jtfirek wants to merge 29 commits into
Conversation
…itive Skeletons for the generic mainnet -> OP listing path so listing a new ERC-20 becomes a single Foundry script + 3CP multisend instead of a per-asset bridge + per-asset oracle integration. Wrapping (mirrors the EtherFiSafeFactory beacon pattern, one shared UpgradeableBeacon impl + cheap per-token CREATE3 beacon proxies): - src/oft/OFTAdapterFactory.sol - mainnet factory - src/oft/ShadowOFTFactory.sol - destination-chain mirror factory - src/oft/EtherFiOFTAdapter.sol - lock-on-deposit beacon impl - src/oft/EtherFiShadowOFT.sol - mintable iTOKEN beacon impl Oracle relay (LZ OApp send/receive, slots into the existing PriceProvider via a Chainlink-shaped latestRoundData accessor; no PriceProvider changes required): - src/oracle/PriceRelay.sol - mainnet OApp sender, reads PriceProvider - src/oracle/OracleSink.sol - destination OApp receiver Interfaces: IOFTAdapterFactory, IShadowOFTFactory, IPriceRelay, IOracleSink (all in src/interfaces/). Deps: adds the lz-address-book submodule (pinned to 747e078, matching the sibling oft-factory repo) and the LayerZero v2 / OApp / OFT upgradeable remappings. Conventions: SPDX MIT, pragma ^0.8.28, ERC-7201 storage with cast-computed slot constants, UpgradeableProxy + RoleRegistry access control, solady CREATE3, NatSpec, custom errors. Core cross-chain logic (OFT _debit/_credit lock/unlock, PriceRelay payload encoding + _lzSend, OracleSink _lzReceive decode) is left as explicit TODO stubs in this skeleton pass. forge build is green; all six new contracts are well under EIP-170. Co-authored-by: Cursor <cursoragent@cursor.com>
- OFTConfigRegistry: canonical per-chain DVN/library config, version counter, bridge enumeration, pushTo/pushToAll/pushToRange (EnumerableSetLib for dstEid + bridge sets) - ConfigurableOFTBase: pull-based syncConfig; self-authorized endpoint.setConfig (no delegate) - EtherFiOFTAdapter / EtherFiShadowOFT: per-proxy ERC-7201 storage for token/decimals/rate so one beacon serves all decimals; adapter fee-on-transfer guard; shadow decimals mirror + placeholder-before-init - Factories: auto-register + syncConfig at deploy (fail-hard); decimals threaded into shadow factory; paginated getters return empty past end Tests deferred (pending review). Refs COR-699.
Fill in the LayerZero send/receive logic in the PriceRelay (mainnet sender) and OracleSink (OP receiver) skeletons. - PriceRelay pays the LZ fee from its own balance (receive/withdraw), so poke is permissionless and non-payable. poke only relays a token once its heartbeat elapsed or its price deviated past the configured threshold, preventing griefers from draining the relay's ETH and implementing the heartbeat + deviation oracle update model on-chain. - OracleSink._lzReceive decodes (tokens, prices, timestamp) and stores each price; peer auth is enforced by OAppReceiver's OnlyPeer check. - IPriceRelay: add heartbeat to TokenSubscription, drop options from poke/quote, add setLzOptions/withdraw/lastRelayed/lzOptions + events/errors. - Add Foundry tests (mock LZ endpoint) covering round-trip, heartbeat, deviation, peer rejection, funding, and OP PriceProvider consumption. Co-authored-by: Cursor <cursoragent@cursor.com>
47 tests (local mock endpoint, no fork): decimals math (6/8/18), fee-on-transfer guard, init guards, OFTConfigRegistry (config/version/push/pagination), and factory auto-sync (register + syncConfig, fail-hard). Also re-sorts OFTAdapterFactory imports (forge fmt). Refs COR-699.
Add adversarial, config-registry robustness/sync, conservation invariant, cross-chain setup, decimals fuzz, factory mechanics, round-trip, and weird-token suites plus the conservation handler. Vendor the LZ v1 ULN interface and add matching remappings.
Replace @inheritdoc with full inline NatSpec on the OFT impls (registry, factories, ConfigurableOFTBase) to match the repo's prevailing convention, and document the decimal-conversion internal overrides on the adapter/shadow impls.
…ss guard - add a RegistryNotSet revert test via a no-guard ConfigurableOFT subclass (both prod impls reject a zero registry at construction, so the guard is otherwise unreachable); closes the one uncovered branch in ConfigurableOFTBase - broaden the conservation invariant from a single 8-decimal pair to 6/8/18-decimal pairs; expose the handler's pair so each pair is asserted independently - fuzz the NonLosslessTransfer lossless guard over amount (and fee bps) instead of a single fixed example
…leness Allow the OP PriceProvider to read the multi-token OracleSink directly via its calldata branch (no per-token Chainlink adapter / no new contract per listed asset). - OracleSink.price(token): returns the relayed 6-decimal USD price and reverts PriceStale once the relay-delivery timestamp exceeds the per-token window - setMaxStaleness(token, seconds) gated by ORACLE_SINK_ADMIN_ROLE (0 disables) - staleness surfaces to the protocol as PriceProvider.price() reverting, same as any other feed - tests: direct read, stale -> revert, disabled-window, admin-gating Co-authored-by: Cursor <cursoragent@cursor.com>
The bridge no longer stores a per-proxy synced version. Sync history is observable via the ConfigSynced event instead, so the ConfigurableOFT storage struct and the syncedConfigVersion() getter are removed from both the interface and the base. Tests updated to assert sync behavior (call count + dstEids) rather than the removed per-proxy version.
The registry version is a global counter, so stamping it into a per-bridge sync event is a leaky freshness signal: editing one destination's pathway bumps the version an unrelated bridge's sync is compared against, flagging that bridge as stale when its own pathway never changed. Per-destination drift is better derived from the dstEid-keyed PathwayConfigSet event, and configVersion() remains on the registry as the source of truth. The event now just reports which dstEids were synced.
Removes named return variables from the OFT implementation contracts in favor of explicit return statements. Interfaces keep their named returns as API documentation; only the implementations are changed.
registerBridge ignored the EnumerableSetLib.add return, so re-registering a known bridge emitted a phantom BridgeRegistered with no state change. Gate the event on the insertion result. Harden the idempotency test to assert no event fires on the duplicate call.
…cases Addresses two Bugbot findings on _deviated, both of which could make poke() relay more often than intended and waste the relay's prefunded LZ balance: - deviationBps == 0 now disables the deviation trigger (heartbeat-only) instead of evaluating `diff * BPS >= 0` (always true) and relaying on every poke. - a previously-relayed price of 0 no longer keeps the deviation branch permanently true; `last == current` short-circuits to no-deviation. - 0 -> non-zero is still treated as an unbounded move and relays. First-send detection moved to an explicit `lastSentAt == 0` check in poke so it no longer relies on `lastSentPrice == 0` overloading. Adds tests for zero-bps (disabled trigger + heartbeat still fires), relayed-zero non-spam, and zero->non-zero recovery. Co-authored-by: Cursor <cursoragent@cursor.com>
…ink-lz-impl feat(oracle): implement PriceRelay + OracleSink LZ price relay
…P) sides Deploys the per-chain OFT listing-primitive infrastructure with an EOA: a dedicated RoleRegistry, OFTConfigRegistry, the beacon implementation (EtherFiOFTAdapter on mainnet / EtherFiShadowOFT on OP), and the factory. RoleRegistry ownership is set to the protocol Safe (OFT_OWNER_MAINNET / OFT_OWNER_OPTIMISM) so the EOA retains no privileged role. Deployed addresses are merged into the per-chain deployments.json, preserving existing keys. Privileged wiring (role grants, pathway config, asset listing, peer wiring) is done separately by the Safe.
feat(oft): mainnet-OP OFT listing primitive
- removePathway / deregisterBridge to retire destinations and bridges - syncBridge: single-bridge re-pull (registrar path) for factory auto-sync - validate DVN sets (sorted, unique, bounded) + optional-DVN threshold - gate syncConfig to the registry (UnauthorizedSync); blocks third-party forced re-pulls
Port the bidirectional PairwiseRateLimiter from the weETH cross-chain work and wire it into both OFT bridges, shipped as a beacon upgrade. Each bridge enforces an independent outbound/inbound cap per peer EID with a linear-decay window. - PairwiseRateLimiter: ERC-7201 namespaced storage (collision-proof for the beacon upgrade), onlyOwner setters, fail-closed (unset pathway = 0 throughput, matching cash-v3's explicit-config convention) - EtherFiOFTAdapter: meter dust-removed amountSentLD in _debit, _amountLD in _credit - EtherFiShadowOFT: add _debit/_credit overrides that meter then call super - scripts: UpgradeOFTRateLimiter (beacon rollout), SetOFTRateLimits (caps in whole tokens scaled to the asset's decimals); listing scripts set default caps inline - tests: fail-closed, linear decay, mid-window limit changes, direction/eid independence, owner-gating, LD metering, and upgrade-preserves-state on both sides
ConfigureAndListOFT{Mainnet,Optimism} only synced an adapter's endpoint
DVN/library rows on first deploy (via the factory's syncBridge). On a
re-run after editing the pathway constants, setPathwayConfig updated the
registry but the skipped deploy meant the live bridge kept stale endpoint
rows. Now the already-listed branch pushes the freshly-set config to the
existing bridge via configRegistry.pushTo, so the script's idempotency
holds under config edits.
Add CheckOFTConfigSync: a read-only, per-chain staleness check that
compares each registered bridge x active destination's effective ULN
config on the LayerZero endpoint against the registry's canonical config,
with REVERT_IF_STALE for CI gating.
_amountCanBeSent computed decay as (_limit * timeSinceLastDeposit) / _window, which overflows the intermediate product when _limit is near type(uint256).max (e.g. an "unlimited" sentinel) and 0 < elapsed < window. Switch to OpenZeppelin Math.mulDiv, which carries the product at full 512-bit precision. Unreachable with realistic token-denominated caps, but removes the footgun and matches the mulDiv convention used elsewhere in the codebase. Fast path is identical to the plain divide, so no meaningful gas/storage cost.
…ating poke() is now payable and relays every subscribed token in one message, with the caller supplying the LayerZero fee (endpoint refunds surplus). This removes the contract-funded balance, so there is nothing to drain and no need for the heartbeat/deviation gating, lastSent tracking, or fund/withdraw machinery the keeper now decides when to relay. Also swaps the stored bytes lzOptions for a uint128 lzReceiveGasLimit (options built inline) and strips poke() modifiers to keep the hot path lean. Storage slot order is preserved (3rd slot bytes -> uint128) so the change is upgrade-safe. Co-authored-by: Cursor <cursoragent@cursor.com>
…ler-pays refactor(oracle): simplify PriceRelay to caller-pays
…ing-scripts feat(oft): registry hardening, EOA scripts, and rate limiter
…oracle peers Replaces the fragmented DeployOFTAdapterSide / DeployOFTShadowSide infra scripts with two single-shot, one-per-chain deploys (primarily the dev bring-up path; ENV-parametrized for dev/mainnet): - DeployOFTListingEthereum: OFTConfigRegistry + EtherFiOFTAdapter beacon + OFTAdapterFactory, plus a dedicated PriceProvider source and the PriceRelay LZ sender. - DeployOFTListingOptimism: OFTConfigRegistry + EtherFiShadowOFT beacon + ShadowOFTFactory, plus the OracleSink LZ receiver. Both reuse the chain's existing cash RoleRegistry instead of a dedicated one. The oracle OApp peers (PriceRelay/OracleSink) deploy via the repo's CREATE3-through-Nick's-factory pattern and each side predicts the counterpart's deterministic address to setPeer in the same broadcast, so the two chains can be deployed in any order. Salts are env-scoped so dev/mainnet don't collide on the same physical chain. The deployer EOA owns the OApp during bring-up to wire the peer, then hands ownership + LZ delegate to OFT_OWNER_<CHAIN> when set. Repoints the kept per-asset scripts (ConfigureAndListOFTMainnet/Optimism, UpgradeOFTRateLimiter) from .addresses.OFTRoleRegistry to .addresses.RoleRegistry. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 42e1ec7. Configure here.
…tate corruption) Co-authored-by: Cursor <cursoragent@cursor.com>
nikkaroraa
force-pushed
the
feat/oft-listing-primitive-boilerplate
branch
from
June 12, 2026 06:51
535b698 to
0284670
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Replaces the fragmented infra deploy scripts with two single-shot, one-per-chain deploys that stand up the OFT listing stack and the oracle bridge, wiring the cross-chain OApp peers in a single pass. These are primarily the dev bring-up path, but are
ENV-parametrized so the same scripts work formainnet.DeployOFTListingEthereum(chainId 1):OFTConfigRegistry+EtherFiOFTAdapterbeacon impl +OFTAdapterFactory, plus a dedicatedPriceProvidersource and thePriceRelayLZ sender.DeployOFTListingOptimism(chainId 10):OFTConfigRegistry+EtherFiShadowOFTbeacon impl +ShadowOFTFactory, plus theOracleSinkLZ receiver.Key decisions
RoleRegistry(read fromdeployments.json) instead of deploying a dedicatedOFTRoleRegistry.PriceProvideras the relay's price source (persisted asRelayPriceProviderso it doesn't clobber the cash one).PriceRelay/OracleSinkdeploy via the repo's CREATE3-through-Nick's-factory pattern; each side predicts the counterpart's address from a shared, env-scoped salt and callssetPeerin the same broadcast, so the two chains can be deployed in any order.setDelegate+transferOwnershiptoOFT_OWNER_<CHAIN>when set (left unset on dev so the EOA keeps ownership).0x1a44…728c; EIDs ETH 30101 / OP 30111.Housekeeping
DeployOFTAdapterSide.s.solandDeployOFTShadowSide.s.sol(replaced).ConfigureAndListOFTMainnet/Optimism,UpgradeOFTRateLimiter) from.addresses.OFTRoleRegistry→.addresses.RoleRegistry.Out of scope (intentionally kept as separate per-asset steps)
Per-asset listing / pathway / DVN config and rate limits stay in
ConfigureAndListOFT*/SetOFTRateLimits. Oracle role grants and token subscriptions are performed by the RoleRegistry owner post-deploy.Test plan
forge buildpasses.DeployOFTListingEthereum/DeployOFTListingOptimismon dev (ENV=dev) and confirm the predicted peer addresses match across both runs.PriceRelay.peers(OP_EID)andOracleSink.peers(ETH_EID)resolve to the deployed counterparts.Made with Cursor