Skip to content
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/personas/web3-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ The boundary between the SDK and the chain. Authoritative rules live in [`AGENTS

### Action-layer purity (§1)

- **State read in the Action layer.** An action whose `buildTx` calls `readContract` or otherwise hits the network — Actions are pure encoders per §1's table. State reads belong in the Entity layer.
- **State read in the Action layer.** An action encoder that calls `readContract` or otherwise hits the network — Actions are pure encoders per §1's table. State reads belong in the Entity layer and are resolved before it returns a `TransactionPlan`.
- **Async in an Action.** The §1 table forbids `async` in actions. Flag any encoder marked `async` (signing belongs at the Client edge, not in encoders).
- **Mutation of input arguments.** Encoders return new objects; mutation of incoming structs is a §1 invariant break.

Expand Down
5 changes: 5 additions & 0 deletions .changeset/liquidity-morpho-six-peer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@morpho-org/liquidity-sdk-viem": patch
---

Widen the Morpho SDK peer dependency range to accept the upcoming v6 transaction-plan release.
5 changes: 5 additions & 0 deletions .changeset/transaction-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@morpho-org/morpho-sdk": major
---

Replace entity action outputs' `{ buildTx, getRequirements }` shape with `TransactionPlan`, exposing semantic transaction/signature steps, ordered executable txs, and intent-based helpers for app labels and flow branching. Derive each prepared plan's accepted signature array from its requirement type so unrelated signature kinds are rejected at compile time.
5 changes: 5 additions & 0 deletions .changeset/wdk-transaction-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@morpho-org/wdk-protocol-lending-morpho-evm": major
---

Adapt Morpho SDK-backed flows to consume `TransactionPlan` outputs while preserving the WDK-facing requirement and transaction methods. Narrow each operation's requirement signature option to the signature kind produced by that operation.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The three pillars: **layering, modularity, testability**. Everything else (types
| Layer | Reads state? | Encodes calldata? | Async? | Returns |
| --- | --- | --- | --- | --- |
| **Client** | no | no | no | factory for entities |
| **Entity** | yes (RPC) | no | yes | lazy `{ buildTx, getRequirements }` |
| **Entity** | yes (RPC) | no | yes | lazy `TransactionPlan` |
| **Action** | no | yes | **no** | deep-frozen `Transaction` |
| **Helpers** | no | encode-only helpers (ABI-only); validators and constants are pure | no | new objects |

Expand Down
204 changes: 110 additions & 94 deletions docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/liquidity-sdk-viem/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"peerDependencies": {
"@morpho-org/blue-sdk": "^6.0.0",
"@morpho-org/blue-sdk-viem": "^5.0.0",
"@morpho-org/morpho-sdk": "^5.0.0",
"@morpho-org/morpho-sdk": "^5.0.0 || ^6.0.0",
"@morpho-org/morpho-ts": "^2.7.0",
"dataloader": "^2.2.3",
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0",
Expand Down
77 changes: 40 additions & 37 deletions packages/morpho-sdk/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ operations on EVM-compatible chains for Morpho protocol.
- **Deterministic transaction building.** Given the same inputs and on-chain state, the SDK
always produces the same `Transaction` object. No simulation, no gas estimation, no
sending — the consumer handles those concerns.
- **Predictable developer experience.** Every operation returns a `{ buildTx, getRequirements }`
pair (for deposits) or `{ buildTx }` (for withdrawals/redeems). The interface is identical
across V1 and V2 vaults.
- **Predictable developer experience.** Every entity operation returns a lazy `TransactionPlan`.
`prepare()` resolves prerequisite transactions and signature requests, while
`PreparedTransactionPlan.build(signatures)` returns the ordered executable transactions. The lifecycle
is identical across VaultV1, VaultV2, Blue, and Midnight.
- **Immutability.** Every returned `Transaction` is deep-frozen via `@morpho-org/morpho-ts`'s
`deepFreeze`. Once built, a transaction object cannot be mutated.
- **No `any`.** Strict TypeScript throughout, with discriminated unions for action types and
Expand Down Expand Up @@ -117,7 +118,7 @@ at the SDK level. The differences are at the protocol layer:
- **Maker routing**: Maker flows build and validate offer trees, collect an Ecrecover root
signature or SetterRatifier transaction, then submit the payload to the Midnight mempool.
- **SDK data**: `MorphoMidnight` fetches hydrated market and position snapshots and exposes
the same lazy `{ getRequirements, buildTx }` contract as the other entities.
the same lazy `TransactionPlan` lifecycle as the other entities.


### Force Deallocation (V2 only)
Expand Down Expand Up @@ -251,58 +252,60 @@ bundle.
### Decision tree

```
getRequirements(viemClient, params)
TransactionPlan.prepare()
├─ supportSignature: false (default)
│ └─► getRequirementsApproval()
│ Spender: generalAdapter1
│ Returns: Transaction<ERC20ApprovalAction>[]
│ • Checks current allowance — skips if sufficient.
│ • For APPROVE_ONLY_ONCE_TOKENS (e.g. USDT): prepends
│ a reset-to-zero approval before the actual approval.
└─ supportSignature: true
├─ Token supports EIP-2612 AND useSimplePermit: true
│ └─► getRequirementsPermit()
│ Returns: Requirement[] with sign() → PermitAction
│ • Checks generalAdapter1 allowance — skips if sufficient.
│ • Produces a signable permit for the generalAdapter1 spender.
└─► getGeneralAdapterRequirements(viemClient, params)
├─ Permit2 contract exists on this chain
│ └─► getRequirementsPermit2()
│ Returns: (Transaction | Requirement)[]
│ Two-step:
│ 1. ERC20 → Permit2: classic approve() if needed (infinite).
│ 2. Permit2 → generalAdapter1: signature if needed or expiring.
├─ supportSignature: false (default)
│ └─► getRequirementsApproval()
│ Spender: generalAdapter1
│ Returns: Transaction<ERC20ApprovalAction>[]
│ • Checks current allowance — skips if sufficient.
│ • For APPROVE_ONLY_ONCE_TOKENS (e.g. USDT): prepends
│ a reset-to-zero approval before the actual approval.
└─ Fallback
└─► getRequirementsApproval() (same as supportSignature: false)
└─ supportSignature: true
├─ Token supports EIP-2612 AND useSimplePermit: true
│ └─► getRequirementsPermit()
│ Returns: Requirement[] with sign() → PermitAction
│ • Checks generalAdapter1 allowance — skips if sufficient.
│ • Produces a signable permit for the generalAdapter1 spender.
├─ Permit2 contract exists on this chain
│ └─► getRequirementsPermit2()
│ Returns: (Transaction | Requirement)[]
│ Two-step:
│ 1. ERC20 → Permit2: classic approve() if needed (infinite).
│ 2. Permit2 → generalAdapter1: signature if needed or expiring.
└─ Fallback
└─► getRequirementsApproval() (same as supportSignature: false)
```

### How signatures flow into deposits

When requirements return a `Requirement` object (permit, permit2, or Morpho authorization path),
the consuming application calls `requirement.sign(client, userAddress)` to obtain a
`RequirementSignature`. The collected signatures are then passed to `buildTx` as an array
(`buildTx([...signatures])`), letting a permit and a Morpho authorization signature travel
together:
`TransactionPlan.prepare()` exposes it as a signature request. The consuming application calls
`request.sign(client, userAddress)` to obtain a `RequirementSignature`, then passes the collected
signatures to `PreparedTransactionPlan.build([...signatures])`, letting a permit and a Morpho
authorization signature travel together:

```
getRequirements() → Requirement { sign() } → RequirementSignature → buildTx([sig, ...])
prepare() → signatureRequests → RequirementSignature → build([sig, ...])
```

Inside `buildTx`, `getTokenRequirementActions()` converts the signature into bundler actions:
Inside the primary transaction builder, `getTokenRequirementActions()` converts the signature into bundler actions:

- **Permit path**: `permit` action + `erc20TransferFrom` to generalAdapter1.
- **Permit2 path**: `approve2` action + `transferFrom2` to generalAdapter1.

These actions are prepended to the `erc4626Deposit` action in the bundle. The entire sequence
executes atomically in a single transaction.

When no signature is provided (classic approval path), `buildTx()` uses a simple
`erc20TransferFrom` action to move tokens from the user to the general adapter before the
deposit.
When no signature is provided (classic approval path), the prepared plan exposes the required
approval as a preparation-phase call and the primary builder uses a simple `erc20TransferFrom`
action to move tokens from the user to the general adapter before the deposit.

### Guard functions

Expand Down
6 changes: 3 additions & 3 deletions packages/morpho-sdk/BUNDLER3.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Instead of exposing the user directly to target contracts (ERC-4626 vault, Morph
- executes `morphoSupplyCollateral`, `morphoBorrow`, `morphoRepay`, `morphoWithdrawCollateral` on Morpho Blue,
- forwards `reallocateTo` calls to the `PublicAllocator` for shared liquidity.

The **spender** of every approval / permit / permit2 is therefore **always** `generalAdapter1`, never the vault or Morpho directly. See [src/actions/requirements/getRequirements.ts](src/actions/requirements/getRequirements.ts) and the "Requirements System" section of [ARCHITECTURE.md](ARCHITECTURE.md#requirements-system).
The **spender** of every approval / permit / permit2 is therefore **always** `generalAdapter1`, never the vault or Morpho directly. See [`getGeneralAdapterRequirements`](src/actions/requirements/generalAdapter/getGeneralAdapterRequirements.ts) and the "Requirements System" section of [ARCHITECTURE.md](ARCHITECTURE.md#requirements-system).

## Composability & modularity

Expand Down Expand Up @@ -70,7 +70,7 @@ For every ERC-4626 deposit (VaultV1 / VaultV2), GeneralAdapter1 calls `erc4626De

### 5. A single approval surface

Whether it's a V1 deposit, a V2 deposit, a `supplyCollateral`, a `repay`, or a `supplyCollateralBorrow`: the spender is **always** `generalAdapter1`. A user who has already approved GA1 for a given token transparently reuses that approval. The approval / permit / permit2 decision is centralized in [`getRequirements`](src/actions/requirements/getRequirements.ts).
Whether it's a V1 deposit, a V2 deposit, a `supplyCollateral`, a `repay`, or a `supplyCollateralBorrow`: the spender is **always** `generalAdapter1`. A user who has already approved GA1 for a given token transparently reuses that approval. The approval / permit / permit2 decision is centralized in [`getGeneralAdapterRequirements`](src/actions/requirements/generalAdapter/getGeneralAdapterRequirements.ts).

## Dangers & limits

Expand Down Expand Up @@ -98,7 +98,7 @@ This is the main design caveat. For the following operations the SDK emits a **d

### Other pitfalls

- **Blue authorization for GA1 required for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`.** A user who has never granted it will receive a requirement through [`getBlueAuthorizationRequirement`](src/actions/requirements/blue/getBlueAuthorizationRequirement.ts). Without signature support, this is a `setAuthorization` transaction to execute beforehand. With `supportSignature`, this is a signable requirement; pass the resulting `AuthorizationRequirementSignature` to `buildTx`, which folds it into the bundle as `setAuthorizationWithSig`.
- **Blue authorization for GA1 required for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`.** A user who has never granted it will receive a request through [`getBlueAuthorizationRequirement`](src/actions/requirements/blue/getBlueAuthorizationRequirement.ts) when the plan is prepared. Without signature support, this is a `setAuthorization` transaction step that appears before the primary step. With `supportSignature`, it is a signable request; pass the resulting `AuthorizationRequirementSignature` to `PreparedTransactionPlan.build()`, which folds it into the bundle as `setAuthorizationWithSig`.
- **Critical order in `repayWithdrawCollateral`**: `morphoRepay` **must** precede `morphoWithdrawCollateral` in the bundle, otherwise the position is deemed unhealthy at withdraw time and the tx reverts.
- **Builder must equal signer.** Bundler actions reference accounts in two different ways: some take an explicit `onBehalf` and act on `userAddress` (e.g. `morphoRepay`), others act implicitly on the **initiator** — the `msg.sender` of `bundler3.multicall`, i.e. the EOA signing the tx, not the adapter — (e.g. `erc20TransferFrom`, `morphoWithdrawCollateral`, the latter exposing no `onBehalf` parameter on GA1). `repayWithdrawCollateral` is the canonical example: the repay leg targets `userAddress` while the transfer-from and the withdraw target the initiator. If the address that built the tx (and filled `userAddress`) is not the address that signs/executes it, the bundle would repay one account's debt while pulling tokens from and withdrawing collateral against the signer. Transaction builders do not validate this at build time — callers MUST keep `userAddress` aligned with the signing account. The signature requirements (`encodeErc20Permit` / `encodeErc20Permit2Approve`) take a `WalletClient` and enforce this at `sign()` time via `validateUserAddress` (throws `MissingClientPropertyError` / `AddressMismatchError`).
- **Tricky `tx.value`**: whenever a `nativeAmount` or a `reallocateTo` (native fee) is involved, `BundlerAction.encodeBundle` computes `tx.value`. Do not overwrite it on the caller side.
Expand Down
Loading