Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 0 additions & 9 deletions .gitattributes

This file was deleted.

34 changes: 25 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,28 @@ The Solidity test helper contract is `evm/tests/foundry/AbiEncodeTest.sol`.

## AI workflow docs

Each package under `sdk/packages/*` keeps AI workflow docs in `docs/ai/`:

- `ChangeLog.md` — append an entry for every AI-assisted code change (date, what changed, files touched).
- `Decisions.md` — record non-obvious choices with the alternatives considered and why they lost.
- `Flow.md` — how the code paths actually execute; update it when documented control flow changes, and add flows as they are read and verified. Never document a flow speculatively.

When changing code in a package that has `docs/ai/`, updating these files is part of the change, not optional follow-up. When starting substantial work in a package that has no `docs/ai/` yet (e.g. simplex, sdk, core, lz-endpoint), create the three files as the first step and seed them from that task's actual work — never with empty templates.

These are not release notes. Package `CHANGELOG.md` files are changesets release logs managed separately; `ChangeLog.md` lives in `docs/ai/` partly because the two names collide on case-insensitive filesystems.
Each package under `sdk/packages/*` keeps AI workflow docs in `docs/ai/`, as one file per
entry. Every package's `docs/ai/README.md` states the conventions; in short:

- `changelog/YYYY-MM-DD-short-title.md` — one file per AI-assisted code change: what changed and why, then a `Files:` line listing the files touched.
- `decisions/YYYY-MM-DD-short-title.md` — one file per non-obvious choice, with the alternatives considered and why they lost.
- `flows/<flow-name>.md` — one file per code path, no date in the name, describing how it actually executes. Update it when the documented control flow changes, and add flows as they are read and verified. Never document a flow speculatively.

Always write a new file for a changelog or decision entry. Never append to an existing one,
and never gather entries back into a shared `ChangeLog.md`-style file — that is the layout
this replaced. Two concurrent PRs appending to one file collide on the same line, and GitHub
blocks the merge: it ignores the `merge=union` driver in `.gitattributes` that resolves the
collision locally, so the conflict is real as far as the PR is concerned. Separate files
cannot collide, so the question never arises.

Flow files are the exception — they are edited in place, so two PRs revising the same flow
do conflict. That conflict is worth seeing, because it means two changes disagree about how
the code runs.

When changing code in a package that has `docs/ai/`, writing these files is part of the
change, not optional follow-up. When starting substantial work in a package that has none
yet (lz-endpoint), create the three directories and the README as the first step and seed
them from that task's actual work — never with empty templates.

These are not release notes. Package `CHANGELOG.md` files are changesets release logs,
managed separately.
287 changes: 0 additions & 287 deletions sdk/packages/core/docs/ai/ChangeLog.md

This file was deleted.

301 changes: 0 additions & 301 deletions sdk/packages/core/docs/ai/Decisions.md

This file was deleted.

22 changes: 22 additions & 0 deletions sdk/packages/core/docs/ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# AI workflow docs — `sdk/packages/core`

AI-maintained notes on this package. One file per entry: two concurrent PRs add
different files, so they never conflict. Never collect entries back into a
shared file.

- `changelog/` — one file per AI-assisted code change, named
`YYYY-MM-DD-short-title.md`. Say what changed and why in a few sentences, then
a `Files:` line listing the files touched.
- `decisions/` — one file per non-obvious choice, named the same way. Give the
decision, the alternatives considered, and why they lost. Read these before
changing related code, so a later change does not silently undo a deliberate
trade-off.
- `flows/` — one file per code path, named for the flow itself and with no date,
because these are edited in place as the code changes. Only document a flow
you have read and verified.

Each file opens with an `#` heading. Changelog and decision names sort
chronologically, so `ls changelog/` reads oldest first and `ls -r` newest first.

This is not the release changelog — that is `CHANGELOG.md` at the package
root, generated by changesets.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 2026-08-27 — `IIntentGatewayV2` brought back in sync with the gateway (#1160)

`IIntentGatewayV2` had drifted from the deployed `IntentGatewayV2`. Every declaration in the
interface is now identical to the one in `evm/src/apps/intentsv2/IntentsBase.sol`, verified by
diffing the two declaration sets:

- `OrderFilled`, `EscrowReleased` and `EscrowRefunded` were still the pre-`tokens` signatures. All
three take a `TokenInfo[]` the interface did not declare.
- `NewDeploymentAdded(bytes stateMachineId, address gateway)` does not exist. The gateway emits
`DeploymentAdded(string chain, address gateway)` — wrong name and wrong parameter type, so a
consumer filtering on it would have matched nothing.
- `PartialFill`, `DestinationProtocolFeeUpdated`, and the `UnknownInstance` and
`PartialFillNotAllowed` errors were absent.
- `OrderCancelled(bytes32 indexed commitment, address canceller)` was added, the event this issue
introduces on the gateway.

Nothing else in the repo compiles against these declarations — `SolverAccount.sol` imports the
interface only for `select.selector` and `fillOrder.selector` — so the change is inert here and
matters to integrators who read the interface as the gateway's published surface.

Files: `contracts/apps/IntentGatewayV2.sol`, `package.json`, `docs/ai/ChangeLog.md`,
`docs/ai/Decisions.md`, `docs/ai/Flow.md`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# 2026-09-03 — Governance deliveries to `HostManager` gated on the same relayer

Closes the route around the app-level gates: `HostManager.onAccept` accepted `SetHostParam` from
any relayer, and that request can replace the host's handler, the contract every app trusts to
report the relayer address. `HostManager` now holds `_relayer`, set by the host admin through
`setRelayer`, and `onAccept` reverts with `UnauthorizedRelayer` for any other relayer, zero
included. Only governance traffic reaches this contract, so ordinary relaying is unaffected.
No file in this package changed; the entry is here because the delivery flow documented in
`Flow.md` is what it corrects.

Files: `evm/src/core/HostManager.sol`, `evm/script/DeployIsmp.s.sol`,
`evm/script/DeployHostManager.s.sol`, `evm/tests/foundry/HostManagerTest.sol`,
`evm/tests/foundry/BaseTest.sol`, `evm/tests/rust/src/tests/utils.rs`, `docs/ai/Decisions.md`,
`docs/ai/Flow.md`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 2026-09-03 — Host manager rotation addressed to the manager the host still trusts

`pallet-ismp-host-executive::update_host_params` applied the update before reading the request
recipient, so a rotation from one HostManager to another was addressed to the new one. The host
accepts `updateHostParams` only from its current manager, so the delivery reverted, Hyperbridge
recorded the new manager anyway, and every later host update and withdrawal for that chain was
sent to a contract the host did not trust. The recipient is now captured before the update; the
payload still installs the new manager. This is the path the HostManager relayer gate is rolled
out through. No file in this package changed.

Files: `modules/pallets/host-executive/src/lib.rs`,
`modules/pallets/testsuite/src/tests/pallet_ismp_host_executive.rs`,
`evm/tests/foundry/HostManagerTest.sol`, `docs/ai/Flow.md`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# 2026-09-03 — Relayer allowlist on `HyperFungibleToken`, fail-closed on the BRIDGE token

`HyperFungibleToken` gains `_relayer`, `relayer()`, `setRelayer(address)` (owner only), the
`RelayerUpdated(address previous, address current)` event, the `UnauthorizedRelayer` error, and a
virtual `_checkRelayer` that `onAccept` and `onPostRequestTimeout` call before anything else. Both
callbacks mint, so both are gated. In the base the check is opt-in: zero leaves deliveries open, so
tokens already deployed from this package behave as before until their owner sets a relayer.
`evm/src/apps/BridgeToken.sol` overrides `_checkRelayer` to fail closed, and its deploy script sets
the relayer from `GOVERNANCE_RELAYER` before `configure`, so the token is never live without one.
`IHyperFungibleToken` is unchanged: `supportsInterface` keys on its `interfaceId`, so adding the
new functions there would change what every existing deployment reports.

Files: `contracts/apps/HyperFungibleToken.sol`, `docs/ai/ChangeLog.md`, `docs/ai/Decisions.md`,
`docs/ai/Flow.md`. Outside the package: `evm/src/apps/BridgeToken.sol`,
`evm/script/DeployBridgeToken.s.sol`, `evm/tests/foundry/BridgeTokenTest.t.sol`,
`evm/tests/foundry/HyperFungibleTokenTest.sol`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 2026-09-03 — Relayer allowlist on the intent gateway

The gateway now accepts `onAccept` and `onGetResponse` deliveries only from a single authorised
relayer stored at `_relayer` (slot 13, packed behind `_paused`). The check runs before the message
body is decoded, so escrow redemptions, refunds and every governance action, upgrades included, are
covered. A refused delivery reverts, which the host records as undelivered, so the authorised
relayer can submit the same message later. `setRelayer(address)` is callable by the immutable
`_owner` and by the host; the host branch exists so a governance `UpgradeContract` can carry the
call as its migration calldata and arm the relayer in the upgrade transaction (`upgradeToAndCall`
delegatecalls that calldata with the host still as `msg.sender`).

The interface gains `RelayerUpdated(address previous, address current)` and `setRelayer`, keeping
its declarations identical to `IntentsBase`. The unused `_paused` getter was dropped from the gateway
to stay under the EIP-170 size limit; it was never declared here.

Files: `contracts/apps/IntentGatewayV2.sol`, `package.json`, `docs/ai/ChangeLog.md`,
`docs/ai/Decisions.md`, `docs/ai/Flow.md`. Gateway side: `evm/src/apps/IntentGatewayV2.sol`,
`evm/src/apps/intentsv2/IntentsBase.sol`, `evm/src/apps/intentsv2/ExtrinsicIntents.sol`,
`evm/tests/foundry/IntentGatewayV2Test.sol`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 2026-09-04 — Fresh gateway deployments arm the relayer in the deploy script

`DeployIntentGateway.s.sol` deployed the gated gateway with `_relayer` unset, so a proxy on a new
chain refused every delivery, including the `upgrade_gateway` message that could have armed it;
only the owner key could, and nothing called it. The script now reads `GATEWAY_RELAYER`, requires
the deploy key to be the admin (the only caller of `setRelayer`), calls `setRelayer` right after
the proxy is deployed, and asserts the relayer afterwards. `initialize` is unchanged so the
deterministic proxy address is unchanged. The gateway constructor now rejects a zero owner, since
a mis-set `ADMIN` would leave no key able to arm a fresh proxy; runtime size is unaffected. No
file in this package changed.

Files: `evm/script/DeployIntentGateway.s.sol`, `evm/src/apps/IntentGatewayV2.sol`,
`evm/tests/foundry/IntentGatewayV2Test.sol`, `docs/ai/Flow.md`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 2026-09-05 — Gateway `initialize` refused on any proxy already at a version

`initialize` carries an `onlyFresh` modifier that reverts with `InvalidInitialization` unless the
`Initializable` version is 0. Without it, an upgrade that installed this implementation on a
version-1 proxy without running `migrate` would leave `initialize`, which has no caller
restriction, open to anyone until governance caught up. Now the host-only `migrate` is the only
way up for such a proxy. `testInitializeRefusedOnLegacyProxy` plays it. The interface NatSpec for
`migrate` says so.

Files: `contracts/apps/IntentGatewayV2.sol`, `docs/ai/ChangeLog.md`, `docs/ai/Decisions.md`,
`docs/ai/Flow.md`. Outside the package: `evm/src/apps/IntentGatewayV2.sol`,
`evm/tests/foundry/IntentGatewayV2Test.sol`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 2026-09-05 — Gateway `initialize` takes the relayer and lands at version 2; `migrate` for older proxies

`IntentGatewayV2.initialize(Params, bytes[] peerChains, address relayer)` now arms the relayer
gate from the init data and runs under `reinitializer(VERSION)` with `VERSION = 2`, so a fresh
proxy comes out armed and at the version of the code it runs. `migrate(address relayer)`, host-only
and under the same `reinitializer(VERSION)`, is for proxies deployed before this implementation:
it arms them and takes them from 1 to 2, and reverts on a proxy `initialize` already took there.
`setRelayer` stays a plain host-only rotation that leaves the version alone; all three write
through `_setRelayer` in `ExtrinsicIntents`. The next implementation that needs a migration bumps
`VERSION` once. The interface documents `migrate` and `version` accordingly.

`DeployIntentGateway.s.sol` always deploys the implementation and the solver account, deploys the
proxy only where `INTENT_GATEWAY_V2` is absent from the chain's config, reads the relayer from
`GATEWAY_RELAYER` for the init data, and records `INTENT_GATEWAY_V2_IMPL`. The relayer is now part
of what fixes a new proxy's address, as the implementation address already was.

The reinitializer cost more than the 71 bytes of EIP-170 headroom, and every gateway getter and
event has a consumer in `sdk`, `simplex` or the indexer, so the room came from deduplicating
internal code with no behaviour change: `_sendValue` in `IntentsBase` for the native
send-and-check (the same-chain fill loop keeps its inline copy, being at the via-ir stack limit),
`_splitSurplus` moved to `IntentsBase` and used by the cross-chain fill, `_withdrawalBody` and
`_postToSource` in `ExtrinsicIntents` for the escrow messages, and `placeOrder` reusing its
`feeToken` read and hashing the order once.

Tests: every `initialize` call gains the relayer argument, `address(0)` outside `setUp` so those
gateways stay open as before; `testInitializeArmsTheGate` pins the events and version; the
`migrate` tests run on a proxy written back to version 1 through the `Initializable` slot, since
this implementation cannot produce one; the live mainnet-fork upgrade migrates the real one.
`HostManager.onAccept` gained NatSpec.

Files: `contracts/apps/IntentGatewayV2.sol`, `docs/ai/ChangeLog.md`, `docs/ai/Decisions.md`,
`docs/ai/Flow.md`. Outside the package: `evm/src/apps/IntentGatewayV2.sol`,
`evm/src/apps/intentsv2/ExtrinsicIntents.sol`, `evm/src/apps/intentsv2/IntrinsicIntents.sol`,
`evm/src/apps/intentsv2/IntentsBase.sol`, `evm/src/core/HostManager.sol`,
`evm/script/DeployIntentGateway.s.sol`, `evm/tests/foundry/IntentGatewayV2Test.sol`,
`evm/tests/foundry/IntentGatewayV2SameChainTest.sol`,
`evm/tests/foundry/IntrinsicIntentsReentrancyTest.sol`,
`evm/tests/foundry/account/SolverAccountTest.sol`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 2026-09-05 — `HostManager` admin is the governance relayer; gateway `setRelayer` is host-only

`HostManager` no longer has a separate relayer. Its `admin` survives initialization and is the only
relayer whose `onAccept` deliveries are accepted, so `_relayer`, `relayer()` and `setRelayer` are
gone. `setIsmpHost` is now `init`: admin-only, one-shot, and unnecessary when the host is passed to
the constructor. A new `SetAdmin` action (variant `2`, body `abi.encode(address)`) rotates the admin
through governance, delivered by the outgoing admin like every other message; zero is refused there
and in the constructor, since a manager with no admin could never be reached again.
`pallet-ismp-host-executive` gains `set_host_manager_admin`, which dispatches that action to the
manager on record, and `ismp-abi` gains `encode_set_admin`. `evm/rust/abi/HostManager.json` was
regenerated; it had not been since before the relayer gate.

On the gateway, `setRelayer` moved from `IntentGatewayV2` to `ExtrinsicIntents` and is `onlyHost`,
so `_owner` can no longer rotate the relayer; the host reaches it only as `UpgradeContract`
migration calldata, and nothing else writes the relayer. `initialize` is unchanged, so a fresh
proxy starts with no relayer, and an unset relayer now gates nothing: the governance upgrade that
arms it has to be delivered first. `setRelayer(address(0))` reopens the gate rather than closing
it. `DeployIntentGateway.s.sol` no longer reads `GATEWAY_RELAYER` or calls `setRelayer`.
`DeployIsmp.s.sol` constructs the host before the manager and binds the manager at construction,
with `GOVERNANCE_RELAYER` as its admin; `DeployHostManager.s.sol` does the same for a replacement
manager. Both contracts expose `relayer()`, and the gateway exposes `version()`, the
`Initializable` version the proxy has reached (1 after `initialize`, higher only after a
`reinitializer` migration), so tooling can tell which relayer a deployment accepts and whether it
has the gate at all (a revert means it predates it). The gateway's `_relayer` and `_instances`
became internal to pay for the getters under EIP-170; `instance(bytes)` already covered the
latter. The interface in this package declares the two getters and updates its `setRelayer`
NatSpec.

Files: `contracts/apps/IntentGatewayV2.sol`, `package.json`, `docs/ai/ChangeLog.md`,
`docs/ai/Decisions.md`, `docs/ai/Flow.md`. Outside the package: `evm/src/core/HostManager.sol`,
`evm/src/apps/IntentGatewayV2.sol`, `evm/src/apps/intentsv2/ExtrinsicIntents.sol`,
`evm/script/DeployIsmp.s.sol`, `evm/script/DeployHostManager.s.sol`,
`evm/script/DeployIntentGateway.s.sol`, `evm/tron/migrations/2_deploy_ismp.js`,
`evm/tron/README.md`, `evm/rust/src/host_params.rs`, `evm/rust/abi/HostManager.json`,
`evm/tests/foundry/HostManagerTest.sol`, `evm/tests/foundry/IntentGatewayV2Test.sol`,
`evm/tests/foundry/IntentGatewayV2SameChainTest.sol`, the foundry test setups that construct a
`HostManager` or initialize a gateway, `evm/tests/rust/src/tests/utils.rs`,
`evm/tests/rust/src/tests/host_manager.rs`, `modules/pallets/host-executive/src/lib.rs`,
`modules/pallets/testsuite/src/tests/pallet_ismp_host_executive.rs`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 2026-09-05 — `HostManager.onAccept` checks caller and relayer through one modifier

`restrict` takes the address to check and the address it must equal, and `onAccept` carries it
twice: `restrict(msg.sender, _params.host)` and `restrict(incoming.relayer, _params.admin)`.
Both failures revert with `UnauthorizedAction`; the separate `UnauthorizedRelayer` error is gone,
and the Foundry and Rust tests expect `UnauthorizedAction` for a wrong relayer. `init` uses the
same two-argument form. `HostManager.json` regenerated.

Files: `docs/ai/ChangeLog.md`. Outside the package: `evm/src/core/HostManager.sol`,
`evm/tests/foundry/HostManagerTest.sol`, `evm/tests/rust/src/tests/host_manager.rs`,
`evm/rust/abi/HostManager.json`.
Loading
Loading