Skip to content

Commit ea10f38

Browse files
committed
feat(morpho-sdk): support midnight token permits
1 parent 6c91c9b commit ea10f38

19 files changed

Lines changed: 1262 additions & 77 deletions

.changeset/quiet-midnight-flows.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ Add Midnight action flows under `client.morpho.midnight(chainId)`, expose Midnig
66

77
The Midnight entity returns lazy action outputs with `getRequirements()` and synchronous `buildTx(...)` methods, matching the existing `morpho-sdk` action pattern while accepting fixed-rate API quote takes directly. UI labels, rate display logic, and offer-chain presentation stay on the integrator side.
88

9-
Midnight Bundles no-permit calls now use the local `PermitKind.None` enum instead of anonymous numeric permit-kind literals.
9+
Midnight Bundles calls support ERC2612 and Permit2 token permits through the same `supportSignature` / `useSimplePermit` requirement flow as Blue, while preserving `PermitKind.None` for approval-based execution.

packages/morpho-sdk/src/actions/midnight/borrowMarket.test.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { midnightBundlesAbi } from "@morpho-org/midnight-sdk";
2-
import { decodeFunctionData } from "viem";
2+
import { decodeFunctionData, type Hex } from "viem";
33
import { describe, expect, test } from "vitest";
44
import {
55
midnightAddresses,
@@ -12,10 +12,14 @@ import {
1212
EmptyMidnightTakesError,
1313
MidnightTakeMarketMismatchError,
1414
MidnightTakeSideMismatchError,
15+
type RequirementSignature,
16+
UnknownMidnightCollateralError,
1517
} from "../../types/index.js";
1618
import { midnightBorrowMarket } from "./borrowMarket.js";
1719
import { PermitKind } from "./types.js";
1820

21+
const signature = `0x${"11".repeat(32)}${"22".repeat(32)}1b` as Hex;
22+
1923
describe("midnightBorrowMarket", () => {
2024
test("default", () => {
2125
const takes = [midnightApiTake({ buy: true })];
@@ -50,6 +54,50 @@ describe("midnightBorrowMarket", () => {
5054
]);
5155
});
5256

57+
test("behavior: encodes collateral token permit", () => {
58+
const tx = midnightBorrowMarket({
59+
chainId: midnightChainId,
60+
market: midnightMarket,
61+
collateralAssets: 2_000n,
62+
loanAssets: 1_000n,
63+
maxUnits: 1_100n,
64+
taker: midnightAddresses.taker,
65+
takes: [midnightApiTake({ buy: true })],
66+
signatures: [
67+
{
68+
action: {
69+
type: "permit",
70+
args: {
71+
spender: midnightAddresses.midnightBundles,
72+
amount: 2_000n,
73+
deadline: 123n,
74+
},
75+
},
76+
args: {
77+
owner: midnightAddresses.taker,
78+
nonce: 0n,
79+
asset: midnightAddresses.collateralToken,
80+
signature,
81+
amount: 2_000n,
82+
deadline: 123n,
83+
},
84+
} satisfies RequirementSignature,
85+
],
86+
});
87+
const decoded = decodeFunctionData({
88+
abi: midnightBundlesAbi,
89+
data: tx.data,
90+
});
91+
92+
expect(decoded.args?.[4]).toMatchObject([
93+
{
94+
permit: {
95+
kind: PermitKind.ERC2612,
96+
},
97+
},
98+
]);
99+
});
100+
53101
test("error: EmptyMidnightTakesError", () => {
54102
expect(() =>
55103
midnightBorrowMarket({
@@ -95,4 +143,19 @@ describe("midnightBorrowMarket", () => {
95143
}),
96144
).toThrow(MidnightTakeMarketMismatchError);
97145
});
146+
147+
test("error: UnknownMidnightCollateralError", () => {
148+
expect(() =>
149+
midnightBorrowMarket({
150+
chainId: midnightChainId,
151+
market: midnightMarket,
152+
collateralAssets: 2_000n,
153+
loanAssets: 1_000n,
154+
maxUnits: 1_100n,
155+
taker: midnightAddresses.taker,
156+
collateralIndex: 1n,
157+
takes: [midnightApiTake({ buy: true })],
158+
}),
159+
).toThrow(UnknownMidnightCollateralError);
160+
});
98161
});

packages/morpho-sdk/src/actions/midnight/borrowMarket.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import {
1010
NegativeMidnightAmountError,
1111
NonPositiveMidnightAmountError,
1212
type Transaction,
13+
UnknownMidnightCollateralError,
1314
} from "../../types/index.js";
14-
import {
15-
type MidnightBorrowMarketParams,
16-
type MidnightCollateralSupply,
17-
PermitKind,
15+
import { encodeMidnightTokenPermit } from "./encodeMidnightTokenPermit.js";
16+
import type {
17+
MidnightBorrowMarketParams,
18+
MidnightCollateralSupply,
1819
} from "./types.js";
1920

2021
/** Encodes the borrow-market Midnight bundle. */
@@ -64,16 +65,29 @@ export const midnightBorrowMarket = (
6465
}
6566

6667
const midnightBundles = getChainAddress(params.chainId, "midnightBundles");
67-
const collateralSupplies: readonly MidnightCollateralSupply[] =
68-
params.collateralAssets > 0n
69-
? [
70-
{
71-
collateralIndex: params.collateralIndex ?? 0n,
72-
assets: params.collateralAssets,
73-
permit: { kind: PermitKind.None, data: "0x" } as const,
74-
},
75-
]
76-
: [];
68+
const market = MarketUtils.toStruct(params.market);
69+
const collateralIndex = params.collateralIndex ?? 0n;
70+
let collateralSupplies: readonly MidnightCollateralSupply[] = [];
71+
if (params.collateralAssets > 0n) {
72+
const collateral = market.collateralParams[Number(collateralIndex)];
73+
if (collateral == null) {
74+
throw new UnknownMidnightCollateralError({
75+
market: marketId,
76+
collateralIndex,
77+
});
78+
}
79+
collateralSupplies = [
80+
{
81+
collateralIndex,
82+
assets: params.collateralAssets,
83+
permit: encodeMidnightTokenPermit({
84+
token: collateral.token,
85+
amount: params.collateralAssets,
86+
signatures: params.signatures,
87+
}),
88+
},
89+
];
90+
}
7791
const receiver = params.receiver ?? params.taker;
7892

7993
let tx = {
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { decodeAbiParameters, type Hex } from "viem";
2+
import { describe, expect, test } from "vitest";
3+
import { midnightAddresses } from "../../../test/fixtures/midnight.js";
4+
import {
5+
DepositAmountMismatchError,
6+
DepositAssetMismatchError,
7+
type RequirementSignature,
8+
} from "../../types/index.js";
9+
import { encodeMidnightTokenPermit } from "./encodeMidnightTokenPermit.js";
10+
import { PermitKind } from "./types.js";
11+
12+
const signature = `0x${"11".repeat(32)}${"22".repeat(32)}1b` as Hex;
13+
14+
describe("encodeMidnightTokenPermit", () => {
15+
test("default", () => {
16+
expect(
17+
encodeMidnightTokenPermit({
18+
token: midnightAddresses.loanToken,
19+
amount: 1_000n,
20+
}),
21+
).toEqual({ kind: PermitKind.None, data: "0x" });
22+
});
23+
24+
test("behavior: encodes ERC2612 signatures", () => {
25+
const permitSignature = {
26+
action: {
27+
type: "permit",
28+
args: {
29+
spender: midnightAddresses.midnightBundles,
30+
amount: 1_000n,
31+
deadline: 123n,
32+
},
33+
},
34+
args: {
35+
owner: midnightAddresses.taker,
36+
nonce: 0n,
37+
asset: midnightAddresses.loanToken,
38+
signature,
39+
amount: 1_000n,
40+
deadline: 123n,
41+
},
42+
} satisfies RequirementSignature;
43+
44+
const permit = encodeMidnightTokenPermit({
45+
token: midnightAddresses.loanToken,
46+
amount: 1_000n,
47+
signatures: [permitSignature],
48+
});
49+
const decoded = decodeAbiParameters(
50+
[
51+
{ type: "uint256" },
52+
{ type: "uint8" },
53+
{ type: "bytes32" },
54+
{ type: "bytes32" },
55+
],
56+
permit.data,
57+
);
58+
59+
expect(permit.kind).toBe(PermitKind.ERC2612);
60+
expect(decoded).toEqual([
61+
123n,
62+
27,
63+
`0x${"11".repeat(32)}`,
64+
`0x${"22".repeat(32)}`,
65+
]);
66+
});
67+
68+
test("behavior: encodes Permit2 signatures", () => {
69+
const permit2Signature = {
70+
action: {
71+
type: "permit2",
72+
args: {
73+
spender: midnightAddresses.midnightBundles,
74+
amount: 1_000n,
75+
deadline: 123n,
76+
expiration: 123n,
77+
},
78+
},
79+
args: {
80+
owner: midnightAddresses.taker,
81+
nonce: 42n,
82+
asset: midnightAddresses.loanToken,
83+
signature,
84+
amount: 1_000n,
85+
deadline: 123n,
86+
expiration: 123n,
87+
},
88+
} satisfies RequirementSignature;
89+
90+
const permit = encodeMidnightTokenPermit({
91+
token: midnightAddresses.loanToken,
92+
amount: 1_000n,
93+
signatures: [permit2Signature],
94+
});
95+
const decoded = decodeAbiParameters(
96+
[{ type: "uint256" }, { type: "uint256" }, { type: "bytes" }],
97+
permit.data,
98+
);
99+
100+
expect(permit.kind).toBe(PermitKind.Permit2);
101+
expect(decoded).toEqual([42n, 123n, signature]);
102+
});
103+
104+
test("error: DepositAssetMismatchError", () => {
105+
const permitSignature = {
106+
action: {
107+
type: "permit",
108+
args: {
109+
spender: midnightAddresses.midnightBundles,
110+
amount: 1_000n,
111+
deadline: 123n,
112+
},
113+
},
114+
args: {
115+
owner: midnightAddresses.taker,
116+
nonce: 0n,
117+
asset: midnightAddresses.collateralToken,
118+
signature,
119+
amount: 1_000n,
120+
deadline: 123n,
121+
},
122+
} satisfies RequirementSignature;
123+
124+
expect(() =>
125+
encodeMidnightTokenPermit({
126+
token: midnightAddresses.loanToken,
127+
amount: 1_000n,
128+
signatures: [permitSignature],
129+
}),
130+
).toThrow(DepositAssetMismatchError);
131+
});
132+
133+
test("error: DepositAmountMismatchError", () => {
134+
const permitSignature = {
135+
action: {
136+
type: "permit",
137+
args: {
138+
spender: midnightAddresses.midnightBundles,
139+
amount: 1_001n,
140+
deadline: 123n,
141+
},
142+
},
143+
args: {
144+
owner: midnightAddresses.taker,
145+
nonce: 0n,
146+
asset: midnightAddresses.loanToken,
147+
signature,
148+
amount: 1_001n,
149+
deadline: 123n,
150+
},
151+
} satisfies RequirementSignature;
152+
153+
expect(() =>
154+
encodeMidnightTokenPermit({
155+
token: midnightAddresses.loanToken,
156+
amount: 1_000n,
157+
signatures: [permitSignature],
158+
}),
159+
).toThrow(DepositAmountMismatchError);
160+
});
161+
});

0 commit comments

Comments
 (0)