Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9c4bbba
feat(FraxFacet): add Frax HopV2 (LayerZero OFT) bridge facet + config…
0xDEnYO Jul 13, 2026
d6f5d58
feat(FraxFacet): harden swap-path guards + Tempo fee sweep; add docs …
0xDEnYO Jul 13, 2026
5204fc3
test(FraxFacet): fork + mock suite, 100% line coverage incl. Tempo ER…
0xDEnYO Jul 13, 2026
42bfa17
feat(FraxFacet): add demo script; flag unverified API slug + fix Temp…
0xDEnYO Jul 13, 2026
0c28eda
fix(FraxFacet): address CodeRabbit review — demo aborts on quote-rout…
0xDEnYO Jul 13, 2026
9b1e149
Merge remote-tracking branch 'origin/main' into feature/exsc-382-frax…
0xDEnYO Jul 20, 2026
9efefa0
feat(FraxFacet): validate destinationChainId via governed EID mapping…
0xDEnYO Jul 20, 2026
091c63a
refactor(config): single-source LayerZero EIDs in config/layerzero.js…
0xDEnYO Jul 20, 2026
f5f1b50
chore(deploy): register FraxFacet v1.0.0 on arbitrum staging (EXSC-382)
0xDEnYO Jul 20, 2026
047a4d6
chore(demoFrax): lower demo amount to 5 frxUSD for staging smoke test…
0xDEnYO Jul 20, 2026
3c47544
docs(demoFrax): record verified arbitrum-staging bridge tx hash (EXSC…
0xDEnYO Jul 20, 2026
5adc7dc
refactor(config): sparse optional per-network config values + _getOpt…
0xDEnYO Jul 20, 2026
c06ffd9
refactor(config): inline LayerZero EIDs per-facet; drop layerzero.jso…
0xDEnYO Jul 21, 2026
8101e8d
feat(config): add abstract (2741 -> EID 30324) to FraxFacet mappings …
0xDEnYO Jul 21, 2026
7a19f30
fix(FraxFacet): address peer review — selector collision, fee over-pu…
0xDEnYO Jul 22, 2026
4daf2f1
chore(deploy): arbitrum staging — redeploy renamed FraxFacet, restore…
0xDEnYO Jul 22, 2026
36fff99
Merge remote-tracking branch 'origin/main' into feature/exsc-382-frax…
0xDEnYO Jul 31, 2026
f08d37f
feat(FraxFacet): address Frax external review — deprecated spokes, no…
0xDEnYO Aug 10, 2026
29b29d8
Merge remote-tracking branch 'origin/main' into feature/exsc-382-frax…
gvladika Aug 10, 2026
cd43e23
refactor(FraxFacet): route EVM recipient cast through LibBytes.toByte…
gvladika Aug 10, 2026
6a25ce9
chore(deploy): FraxFacet v1.0.0 to arbitrum + base staging (EXSC-382)
gvladika Aug 10, 2026
3ae7e5f
Merge remote-tracking branch 'origin/main' into feature/exsc-382-frax…
gvladika Aug 12, 2026
9f7ec7e
fix(FraxFacet): Frax-scope immutable getter selectors (audit sujithso…
gvladika Aug 17, 2026
fc95b1a
fix(FraxFacet): clarify Tempo fee-token funding is caller approve, no…
gvladika Aug 17, 2026
c87fcab
docs(FraxFacet): drop 'to be confirmed' hedge from Tempo fee-funding …
gvladika Aug 17, 2026
e4aa5c1
docs(FraxFacet): trim Tempo fee-funding note to the approval requirem…
gvladika Aug 17, 2026
0cf0cd0
fix(FraxFacet): cap Tempo LZ fee via BE-supplied nativeFee instead of…
gvladika Aug 18, 2026
78610f4
chore(audit): file FraxFacet v1.0.0 audit report (EXSC-382)
gvladika Aug 20, 2026
96e3731
Merge remote-tracking branch 'origin/main' into audit/fraxfacet-remed…
gvladika Aug 20, 2026
402ac18
chore(clear-signing): regenerate proposal after main merge (EXSC-382)
gvladika Aug 20, 2026
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 .agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Users can either use the `/analyze-tx <network> <tx_hash>` command directly or t
| `001-project-structure.md` | Project structure and file placement guidance | ✅ Always | - |
| `002-architecture.md` | Core architectural principles (Diamond, separation, governance) | ✅ Always | - |
| `003-context-monitor.md` | Context window monitoring and handoff management | ✅ Always | - |
| `004-config-structure.md` | Config JSON structure (key-first vs network-first), deploy paths | ❌ On match | `config/**/*.json`, `script/deploy/**/*.s.sol`, `script/deploy/resources/deployRequirements.json` |
| `004-config-structure.md` | Config JSON structure (key-first vs network-first), required-vs-optional value sparsity, deploy paths | ❌ On match | `config/**/*.json`, `script/deploy/**/*.s.sol`, `script/deploy/resources/deployRequirements.json` |
| `010-agents-authoring.md` | Auto-enforced constraints when editing .agents/ rules or commands | ❌ On match | `.agents/rules/*.md`, `.agents/commands/*.md` |
| `099-finish.md` | Completion checklist to keep repo green | ✅ Always | - |
| `100-solidity-basics.md` | Baseline rules for all Solidity files | ❌ On match | `**/*.sol` |
Expand Down
9 changes: 9 additions & 0 deletions .agents/rules/004-config-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ Example:
}
```

### Required vs optional per-network values (sparsity)

Decide per key whether `address(0)` is a valid value, and shape the config accordingly:

- **Required (`address(0)` is invalid).** The parameter must be a real, code-bearing contract on every supported chain (e.g. `hop`, `htlcRegistry`, `airlift`). **List every network explicitly** and read it with the strict overload `_getConfigContractAddress(path, key)`, which reverts on zero or non-contract. A missing or zero entry must fail the deploy loudly.
- **Optional (`address(0)` is the normal value).** The parameter is meaningful on only a few chains and legitimately zero everywhere else (e.g. Tempo's `tipFeeManager` / `pathUsd`, non-zero only on Tempo). **List only the non-zero networks** — do not enumerate dozens of `0x0000…0000` entries. Read these with `_getOptionalConfigContractAddress(path, key)`, which returns `address(0)` when the key is absent, so a chain omitted from the map deploys with the zero default instead of reverting.

Rationale: `_getConfigContractAddress` reverts on a **missing** key (an absent `readAddress` decodes empty bytes), so an all-zero-but-listed map is not just noise — combined with the fact that `/add-network` does not touch bridge configs, it turns the first deploy on any future chain into a revert on a value that was always meant to be zero. Sparse config for optional keys removes that footgun; explicit listing for required keys keeps a genuinely missing address failing closed. (The bash `checkDeployRequirements` layer already tolerates a missing key when `allowToDeployWithZeroAddress` is `"true"` — set it so for optional keys.)

### Consistency

- When adding or restructuring a config file, update the corresponding deploy script(s) (including `script/deploy/zksync/` when present) and the relevant contract entry in `deployRequirements.json` so `keyInConfigFile` matches the chosen structure (`.<key>.<NETWORK>` vs `.<NETWORK>.<key>`).
1 change: 1 addition & 0 deletions .agents/rules/102-facets.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ paths:

- Location: `src/Facets/`; name contains `Facet`.
- Required functions: `_startBridge` (internal), `swapAndStartBridgeTokensVia{FacetName}`, `startBridgeTokensVia{FacetName}`.
- Facet-scoped selector naming ([CONV:FACET-SELECTORS]): every **external/public** function that is specific to one facet — admin setters, getters, initializers, config methods — must carry the facet/bridge identifier in its name (e.g. `setFraxChainIdToEid`, `getFraxChainIdToEid`, `initFrax`), never a generic name (`setChainIdToEid`, `getChainIdToEid`). Bare names collide by selector when two facets copy-paste the same config subsystem, and `UpdateScriptBase.buildDiamondCut` silently reassigns a colliding selector to the newly-cut facet **and removes every other selector of the previously-owning facet** — uninstalling a live bridge facet at deploy time with no error. The `startBridgeTokensVia{FacetName}` convention already enforces this for entrypoints; apply the same rule to *all* facet-specific functions. When adding a facet whose logic is derived from an existing one, diff its external selectors against the source facet and rename any that match.
- Modifiers: `nonReentrant`, `refundExcessNative`, `validateBridgeData`, `doesNotContainSourceSwaps`/`doesContainSourceSwaps`, `doesNotContainDestinationCalls`/`doesContainDestinationCalls`.
- Parameter handling:
- `receiverAddress` first in `{facetName}Data`, must match `bridgeData.receiver` (EVM).
Expand Down
177 changes: 176 additions & 1 deletion config/clearSigningProposal.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$note": "ERC-7730 display.formats entries for the LIFI Diamond, generated by tasks/buildClearSigningProposal.ts from Foundry artifacts. Source of truth for clear-signing UX strings. Consumed by tasks/generateLedgerClearSigning.ts during the sync workflow: entries here are merged into the registry payload pushed to ethereum/clear-signing-erc7730-registry.",
"$count": 94,
"$count": 96,
"formats": {
"startBridgeTokensViaAcross((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 minAmount, uint256 destinationChainId, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (int64 relayerFeePct, uint32 quoteTimestamp, bytes message, uint256 maxCount) _acrossData)": {
"intent": "Bridge via Across",
Expand Down Expand Up @@ -734,6 +734,79 @@
}
]
},
"startBridgeTokensViaFrax((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 minAmount, uint256 destinationChainId, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (address oft, uint32 dstEid, uint256 nativeFee, address refundRecipient, bytes32 nonEVMReceiver) _fraxData)": {
"intent": "Bridge via Frax",
"interpolatedIntent": "Bridge {_bridgeData.minAmount} via Frax to chain {_bridgeData.destinationChainId} for {_bridgeData.receiver}",
"fields": [
{
"path": "_bridgeData.minAmount",
"label": "Amount to Bridge",
"format": "tokenAmount",
"params": {
"tokenPath": "_bridgeData.sendingAssetId"
},
"visible": "always"
},
{
"path": "_bridgeData.destinationChainId",
"label": "Destination Chain",
"format": "raw",
"visible": "always"
},
{
"path": "_bridgeData.receiver",
"label": "Recipient",
"format": "addressName",
"params": {
"types": [
"eoa",
"contract"
],
"sources": [
"local",
"ens"
]
},
"visible": "always"
},
{
"path": "_fraxData.nonEVMReceiver",
"label": "Non-EVM Recipient",
"format": "raw",
"visible": "always"
},
{
"path": "_bridgeData.transactionId",
"label": "Transaction Id",
"visible": "never"
},
{
"path": "_bridgeData.bridge",
"label": "Bridge",
"visible": "never"
},
{
"path": "_bridgeData.integrator",
"label": "Integrator",
"visible": "never"
},
{
"path": "_bridgeData.referrer",
"label": "Referrer",
"visible": "never"
},
{
"path": "_bridgeData.hasSourceSwaps",
"label": "Has Source Swaps",
"visible": "never"
},
{
"path": "_bridgeData.hasDestinationCall",
"label": "Has Destination Call",
"visible": "never"
}
]
},
"startBridgeTokensViaGarden((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 minAmount, uint256 destinationChainId, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (address redeemer, address refundAddress, uint256 timelock, bytes32 secretHash, bytes32 nonEvmReceiver) _gardenData)": {
"intent": "Bridge via Garden",
"interpolatedIntent": "Bridge {_bridgeData.minAmount} via Garden to chain {_bridgeData.destinationChainId} for {_bridgeData.receiver}",
Expand Down Expand Up @@ -3540,6 +3613,108 @@
}
]
},
"swapAndStartBridgeTokensViaFrax((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 minAmount, uint256 destinationChainId, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (address callTo, address approveTo, address sendingAssetId, address receivingAssetId, uint256 fromAmount, bytes callData, bool requiresDeposit)[] _swapData, (address oft, uint32 dstEid, uint256 nativeFee, address refundRecipient, bytes32 nonEVMReceiver) _fraxData)": {
"intent": "Swap & Bridge via Frax",
"interpolatedIntent": "Swap then bridge {_bridgeData.minAmount} via Frax to chain {_bridgeData.destinationChainId} for {_bridgeData.receiver}",
"fields": [
{
"path": "_swapData.[0].fromAmount",
"label": "Amount to Swap",
"format": "tokenAmount",
"params": {
"tokenPath": "_swapData.[0].sendingAssetId"
},
"visible": "always"
},
{
"path": "_bridgeData.minAmount",
"label": "Minimum to Bridge",
"format": "tokenAmount",
"params": {
"tokenPath": "_bridgeData.sendingAssetId"
},
"visible": "always"
},
{
"path": "_bridgeData.destinationChainId",
"label": "Destination Chain",
"format": "raw",
"visible": "always"
},
{
"path": "_bridgeData.receiver",
"label": "Recipient",
"format": "addressName",
"params": {
"types": [
"eoa",
"contract"
],
"sources": [
"local",
"ens"
]
},
"visible": "always"
},
{
"path": "_fraxData.nonEVMReceiver",
"label": "Non-EVM Recipient",
"format": "raw",
"visible": "always"
},
{
"path": "_bridgeData.transactionId",
"label": "Transaction Id",
"visible": "never"
},
{
"path": "_bridgeData.bridge",
"label": "Bridge",
"visible": "never"
},
{
"path": "_bridgeData.integrator",
"label": "Integrator",
"visible": "never"
},
{
"path": "_bridgeData.referrer",
"label": "Referrer",
"visible": "never"
},
{
"path": "_bridgeData.hasSourceSwaps",
"label": "Has Source Swaps",
"visible": "never"
},
{
"path": "_bridgeData.hasDestinationCall",
"label": "Has Destination Call",
"visible": "never"
},
{
"path": "_swapData.[].callData",
"label": "Swap Data Call Data",
"visible": "never"
},
{
"path": "_swapData.[].callTo",
"label": "Swap Data Call To",
"visible": "never"
},
{
"path": "_swapData.[].approveTo",
"label": "Swap Data Approve To",
"visible": "never"
},
{
"path": "_swapData.[].requiresDeposit",
"label": "Swap Data Requires Deposit",
"visible": "never"
}
]
},
"swapAndStartBridgeTokensViaGarden((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 minAmount, uint256 destinationChainId, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (address callTo, address approveTo, address sendingAssetId, address receivingAssetId, uint256 fromAmount, bytes callData, bool requiresDeposit)[] _swapData, (address redeemer, address refundAddress, uint256 timelock, bytes32 secretHash, bytes32 nonEvmReceiver) _gardenData)": {
"intent": "Swap & Bridge via Garden",
"interpolatedIntent": "Swap then bridge {_bridgeData.minAmount} via Garden to chain {_bridgeData.destinationChainId} for {_bridgeData.receiver}",
Expand Down
121 changes: 121 additions & 0 deletions config/frax.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
{
"DEV NOTICE": "after adding a new mapping, call initFrax (first deploy, wired into UpdateFraxFacet) or setFraxChainIdToEid (incremental update) on every diamond where FraxFacet is deployed. Every destination chain a route targets MUST be seeded here first (unseeded destinations revert UnsupportedChainId). Source for LayerZero EIDs: https://docs.layerzero.network/v2/deployments/deployed-contracts. \"tipFeeManager\"/\"pathUsd\" are intentionally sparse: they list ONLY chains where the value is non-zero (currently just Tempo). Any chain absent from these maps deploys with address(0) — do NOT pad them with zero entries for other networks (see rule 004-config-structure). berachain, mode and scroll are intentionally absent from \"hop\" and \"mappings\": Frax is deprecating them from the hop mesh (confirmed by the Frax team on PR #2048) — do NOT re-add them. Removing a chain here does NOT disable it on diamonds already seeded via initFrax: call unsetFraxChainIdToEid(chainIds) on each of them. chainId 1151111081099710 is LI.FI's Solana chain ID (non-EVM, bytes32 recipient) — routes via the Fraxtal hub to LayerZero EID 30168.",
"hop": {
"mainnet": "0x0000006D38568b00B457580b734e0076C62de659",
"abstract": "0x0000006D38568b00B457580b734e0076C62de659",
"arbitrum": "0x0000006D38568b00B457580b734e0076C62de659",
"avalanche": "0x0000006D38568b00B457580b734e0076C62de659",
"base": "0x0000006D38568b00B457580b734e0076C62de659",
"bsc": "0x0000006D38568b00B457580b734e0076C62de659",
"fraxtal": "0x00000000e18aFc20Afe54d4B2C8688bB60c06B36",
"hyperevm": "0x0000006D38568b00B457580b734e0076C62de659",
"ink": "0x0000006D38568b00B457580b734e0076C62de659",
"katana": "0x0000006D38568b00B457580b734e0076C62de659",
"linea": "0x0000006D38568b00B457580b734e0076C62de659",
"monad": "0x0000006D38568b00B457580b734e0076C62de659",
"optimism": "0x0000006D38568b00B457580b734e0076C62de659",
"plume": "0x0000006D38568b00B457580b734e0076C62de659",
"polygon": "0x0000006D38568b00B457580b734e0076C62de659",
"sei": "0x0000006D38568b00B457580b734e0076C62de659",
"somnia": "0x0000006D38568b00B457580b734e0076C62de659",
"sonic": "0x0000006D38568b00B457580b734e0076C62de659",
"stable": "0x0000006D38568b00B457580b734e0076C62de659",
"tempo": "0x0000006D38568b00B457580b734e0076C62de659",
"unichain": "0x0000006D38568b00B457580b734e0076C62de659",
"worldchain": "0x0000006D38568b00B457580b734e0076C62de659",
"xlayer": "0x0000006D38568b00B457580b734e0076C62de659",
"zksync": "0x0000006D38568b00B457580b734e0076C62de659"
},
"tipFeeManager": {
"tempo": "0xfeEC000000000000000000000000000000000000"
},
"pathUsd": {
"tempo": "0x20C0000000000000000000000000000000000000"
},
"mappings": [
{
"chainId": 1,
"lzEid": 30101
},
{
"chainId": 10,
"lzEid": 30111
},
{
"chainId": 56,
"lzEid": 30102
},
{
"chainId": 130,
"lzEid": 30320
},
{
"chainId": 137,
"lzEid": 30109
},
{
"chainId": 143,
"lzEid": 30390
},
{
"chainId": 146,
"lzEid": 30332
},
{
"chainId": 196,
"lzEid": 30274
},
{
"chainId": 252,
"lzEid": 30255
},
{
"chainId": 324,
"lzEid": 30165
},
{
"chainId": 480,
"lzEid": 30319
},
{
"chainId": 999,
"lzEid": 30367
},
{
"chainId": 1329,
"lzEid": 30280
},
{
"chainId": 2741,
"lzEid": 30324
},
{
"chainId": 8453,
"lzEid": 30184
},
{
"chainId": 42161,
"lzEid": 30110
},
{
"chainId": 43114,
"lzEid": 30106
},
{
"chainId": 57073,
"lzEid": 30339
},
{
"chainId": 59144,
"lzEid": 30183
},
{
"chainId": 98866,
"lzEid": 30370
},
{
"chainId": 1151111081099710,
"lzEid": 30168
}
]
}
15 changes: 12 additions & 3 deletions config/superset.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@
"unichain": "0x326D0B0357b394EC5bbeFE87554243E5dc76e56E"
},
"mappings": [
{ "chainId": 42161, "lzEid": 30110 },
{ "chainId": 8453, "lzEid": 30184 },
{ "chainId": 130, "lzEid": 30320 }
{
"chainId": 130,
"lzEid": 30320
},
{
"chainId": 8453,
"lzEid": 30184
},
{
"chainId": 42161,
"lzEid": 30110
}
]
}
1 change: 1 addition & 0 deletions deployments/arbitrum.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,6 @@
"LayerSwapFacet": "0x6FB56233842489D772b9Bb004390abF317Efa390",
"FeeForwarder": "0x7C1923FbD89154156DE2220870C834B141bA8c49",
"SupersetFacet": "0xD04A7f8EdB8F4C1dB851bCc919e71dB5E5093fc8",
"FraxFacet": "0xCDFE2fc28F6f824F9E619728A52363bd558aB4d0",
"LiFiIntentEscrowFacetV2": "0x1aAADa7ab2BD56de2482d46213AB0994a0C7ff9b"
}
2 changes: 1 addition & 1 deletion docs/ClearSigningProposal.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The extra field is `bytes`/`bytes32` (it may hold a non-EVM address such as a So
Two shapes, keyed by `bridgeFacetName()`:

- **TYPE-A** — the field is the on-chain recipient in **every** branch (always populated). `AcrossV4` (`_acrossData.receiverAddress`) and `DeBridgeDln` (`_deBridgeData.receiver`) can differ from `_bridgeData.receiver` even on EVM (destination call → our Receiver contract; DeBridgeDln never cross-checks its `receiver`), so the field adds signal on EVM too. `Glacis`, `AllBridge`, `LiFiIntentEscrowV2`, `GasZip` enforce equality on plain EVM transfers, so there the field re-shows the recipient and carries the real value on non-EVM. Labelled `"<Bridge> Recipient"`.
- **TYPE-B** — a `nonEVMReceiver`-style field only set on non-EVM transfers (`0x0` on EVM, where `_bridgeData.receiver` is the faithful recipient): `Mayan`, `LayerSwap`, `Chainflip`, `Eco`, `NEARIntents`, `PolymerCCTP`. Labelled `"Non-EVM Recipient"` so the `0x0` on an EVM transfer reads as "N/A".
- **TYPE-B** — a `nonEVMReceiver`-style field only set on non-EVM transfers (`0x0` on EVM, where `_bridgeData.receiver` is the faithful recipient): `Mayan`, `LayerSwap`, `Chainflip`, `Eco`, `NEARIntents`, `PolymerCCTP`, `Frax`. Labelled `"Non-EVM Recipient"` so the `0x0` on an EVM transfer reads as "N/A".

> **`GasZip` caveat:** its `receiverAddress` is a **right-padded** `bytes32` (`bytes32(bytes20(addr))`), unlike the left-padded convention elsewhere; `raw` shows the address in the high bytes.

Expand Down
Loading
Loading