KeeperHub / KeeperHub/keeperhub
feat(protocols): add LayerZero OFT send, paying the fee quoted by oft-quote-send
- Dominant language
- TypeScript
- Stars
- 24
- Forks
- 93
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 266
Description
### Before filing
- [x] I searched open and closed issues for this proposal.
- [x] I checked the docs and the current behaviour on `staging`.
- [x] This is one change, not several. (Several means several issues.)
### Reason: what you cannot do today
A workflow cannot move an OFT across chains with a LayerZero protocol action. It can quote the transfer (`oft-quote-send`, `oft-quote-oft`), check peers and approvals, and approve, but there is no `send`.
The `send` actions were held back on purpose, and every condition for adding them is now met:
- `protocols/layerzero.ts` header: "The two payable send actions depend on the protocol write step being able to convert a wei-denominated quote into the ether-denominated ETH Value field, which is a separate change; they are added once that lands."
- On #2308, suisuss wrote: "Waits on #2307: the two `send` actions. #2307 is accepted, so this is a sequencing dependency rather than an open question."
- #2307 closed and #2322 merged, which added `weiToEther` and the ethValue transform path in `plugins/protocol/steps/protocol-write.ts`.
- `lib/protocol-encode-transforms.ts:297-300` still reads "The payable send actions register here too once they exist." Today no action registers `weiToEther`.
Checked on `staging` at `af80267`.
### Reason: what the workaround costs
The only route is the generic Write Contract action with a hand-supplied ABI. Its Payable Value field is in whole ether ("Specified in whole units, not wei"), while `quoteSend` returns `nativeFee` in wei, and a template cannot convert between them. So a workflow cannot feed a live quote into the send. The user has to hardcode a fee, which is either overpaid or reverts when gas prices move. This is the exact gap #2307 was opened to close.
### Scope: what this touches, and what it does not
Touches:
- `protocols/abis/layerzero-oft.json`: add `send((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),(uint256,uint256),address)` (payable, selector `0xc7c7f5b3`) and the `OFTSent` event.
- `protocols/layerzero.ts`: an `oft-send` action on the existing `oft` contract, reusing `SEND_PARAM_INPUT_OVERRIDES`, with test data and expectations.
- `lib/protocol-encode-transforms.ts`: `padAddressToBytes` on `oft-send/to`, and `weiToEther` on `oft-send/ethValue`. That is the virtual field the transform is legal on; the guard added in #2322 refuses it on ABI inputs.
- `lib/protocol-registry.ts`: a small additive change so a payable action can declare its value field's label and help text. Today every payable action gets a hardcoded `"ETH Value"` field with placeholder `0.0` (`lib/protocol-registry.ts:428-434`). With `weiToEther` registered, this action's field is entered in wei, and labelling it ETH would invite a 10^18 mistake in the other direction. Actions that declare nothing keep today's field unchanged.
- `docs/plugins/layerzero.md`, and unit, calldata golden and on-chain fork tests alongside the existing LayerZero suites.
Does not touch:
- **CCIP `ccip-send`.** It has the same wei-quote gap, but existing workflows type ether into its ETH Value today, so registering `weiToEther` there would break them. If wanted, that is a separate issue with a migration story.
- **Paying in ZRO** (`payInLzToken: true`) and custom compose payloads beyond the existing defaults.
- **Deriving the endpoint ID from the network select.** suisuss ruled this out on #2308 as a core change; `dstEid` stays a typed input with the EID table in the help text, as the quote actions already have it.
- Stargate-specific pools. Anything that is an OFT is covered; nothing Stargate-only is added.
One change: the registry label hook exists only to make this action's wei field honest, so it ships with it.
### Plan: what you propose
`oft-send` inputs:
- The flattened `SendParam`, identical to `oft-quote-send`, so a workflow can pass the same values to both.
- `_fee.nativeFee` and `_fee.lzTokenFee` (the latter defaulting to 0 and marked advanced).
- `refundAddress`.
- The value field, labelled "Messaging Fee (wei)". Its help text says to reference the quote's `fee.nativeFee`, and to pass the same value to both it and `_fee.nativeFee`.
The two must match exactly. In LayerZero-v2 `OAppSender.sol`, `_payNative` is `if (msg.value != _nativeFee) revert NotEnoughNative(msg.value);`, so overpaying reverts as well as underpaying.
Approval stays a separate step, as today: `oft-approval-required` tells the user whether to run `oft-approve` first. On chain 1 (the USDT0 OFT adapter) it returns true.
Evidence gathered on 2026-09-15 against Ethereum mainnet, read-only:
- `quoteSend` on the USDT0 adapter `0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee` to Arbitrum (EID 30110), for 1 USDT with the default Type 3 options, returned `nativeFee = 218756042576226` wei (0.000219 ETH) and `lzTokenFee = 0`. That is the wei value the send's value field must receive.
- A real user transfer, `0x23b8fd4b6be3f30c3784eaaf01bfc038e81e54df142c1dbee28f4c6c03446f6d`, called the adapter directly with selector `0xc7c7f5b3` and `msg.value = 284266317477384` wei, emitting `OFTSent` to EID 30109. So `send` is reachable on the adapter as a direct call. The adapter is a proxy, so the selector is not in its own bytecode.
- An unfunded `eth_call` of `send` reverts without data, as expected with no USDT balance or allowance. The fork test therefore needs a funded actor, the way `sky.ts` provisions tokens through `requiredTokens`.
Tests:
- **Unit:** the action is derived with the expected inputs; both transforms are registered, and `weiToEther` sits only on `ethValue`.
- **Calldata golden:** the encoded send matches a hand-encoded reference.
- **On-chain fork:** approve, quote, then send on chain 1, asserting an `OFTSent` receipt.
Questions for triage:
1. @damli40, you wrote the LayerZero read surface. Is `send` already on your list? If so, I will stand down or take whatever part suits you.
2. Is a label and help-text hook on the payable value field the shape you want? The alternative is a dedicated `protocol-wei-value` field type.
3. Is the chain-1 USDT0 adapter the right fork-test target, given USDT's non-zero-to-non-zero approve rule documented in the existing test data?
### Plan: alternatives you considered
- **Registering `weiToEther` on `ccip-send` and adding LayerZero the same way.** Rejected for CCIP: it changes what existing workflows send.
- **A wei-denominated field only in the docs, keeping the "ETH Value" label.** Rejected: the label is what a user reads at the moment they type the number.
- **Converting inside the template language.** Rejected: templates have no unit arithmetic, and adding it is a core change.
- **Doing nothing.** Leaves the quote actions with no action to feed.
### Scope: compatibility
- [ ] Changes an existing response shape, status code, CLI flag, or default.
- [ ] Adds, removes, or upgrades a dependency.
- [ ] Changes database schema or requires a migration.
- [ ] Touches authentication, permissions, validation, or spend limits.
- [ ] Changes pricing, plan limits, or anything a user is charged.
Contributor guide
Research direction
Start with protocols/layerzero.ts and the existing LayerZero unit, calldata-golden, and fork-test suites, then read lib/protocol-encode-transforms.ts and lib/protocol-registry.ts for the payable-field behavior. Check the OFT ABI and docs/plugins/layerzero.md alongside the existing quote and approval actions. Done means the oft-send action, wei transforms, registry labeling, documentation, and approve-quote-send fork coverage match the stated expectations.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- blockchain
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100