@dev.sail.money/sailor 1.3.0-227 → 1.3.0-228
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/README.md +59 -498
- package/package.json +1 -5
- package/packages/cli/dist/index.cjs +0 -14
- package/packages/sdk/dist/fees.d.ts +2 -1
- package/packages/sdk/dist/fees.d.ts.map +1 -1
- package/packages/sdk/dist/fees.js +2 -1
- package/packages/sdk/dist/fees.js.map +1 -1
- package/packages/sdk/dist/intelligence.d.ts +1 -1
- package/packages/sdk/dist/intelligence.js +1 -1
- package/packages/sdk/package.json +2 -2
- package/templates/default/.agents/skills/sail-mandates/references/examples-index.md +1 -1
- package/templates/default/.agents/skills/sail-template-swap/SKILL.md +2 -2
- package/templates/default/.agents/skills/sail-template-swap-no-oracle/SKILL.md +2 -2
- package/templates/default/.agents/skills/sail-templates/SKILL.md +2 -2
- package/templates/default/docs/PERMISSION_MODEL.md +2 -2
- package/docs/PERMISSION_MODEL.md +0 -93
- package/examples/README.md +0 -24
- package/examples/lifi-permissions/LifiBoundedApprovePermissionCloneable.sol +0 -84
- package/examples/lifi-permissions/LifiDiamondSwapPermissionCloneable.sol +0 -97
- package/examples/lifi-permissions/README.md +0 -53
- package/scripts/check-docs.mjs +0 -309
- package/scripts/check-init.mjs +0 -123
- package/scripts/check-update.mjs +0 -177
- package/scripts/clean.mjs +0 -17
- /package/{examples → templates/default/examples}/custom-mandate/.sail/contracts/interfaces/IPermission.sol +0 -0
- /package/{examples → templates/default/examples}/custom-mandate/README.md +0 -0
- /package/{examples → templates/default/examples}/custom-mandate/foundry.toml +0 -0
- /package/{examples → templates/default/examples}/custom-mandate/mandates/BoundedCallPermission.sol +0 -0
- /package/{examples → templates/default/examples}/custom-mandate/mandates/README.md +0 -0
- /package/{examples → templates/default/examples}/custom-mandate/mandates/SailCalldata.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedApproveAndCallBatch.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedBorrow_AaveV3_Arbitrum.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedLimitless_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedPerp_GMXv2_Arbitrum.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedStake_Venice_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedSupply_AaveV3_Arbitrum.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedSwapNative_UniswapV3_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV3_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV4_Unichain.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedTransfer_ERC20_Ethereum.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedVault_ERC4626_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/README.md +0 -0
- /package/{examples → templates/default/examples}/permissions/SailCalldata.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/foundry.toml +0 -0
- /package/{examples → templates/default/examples}/permissions/interfaces/IBatchPermission.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/interfaces/IPermission.sol +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dev.sail.money/sailor",
|
|
3
|
-
"version": "1.3.0-
|
|
3
|
+
"version": "1.3.0-228",
|
|
4
4
|
"description": "Operator toolkit for Sail Protocol",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sailor": "packages/cli/dist/index.cjs"
|
|
@@ -17,9 +17,6 @@
|
|
|
17
17
|
"packages/sdk/package.json",
|
|
18
18
|
"packages/ui/dist",
|
|
19
19
|
"templates",
|
|
20
|
-
"examples",
|
|
21
|
-
"docs",
|
|
22
|
-
"scripts",
|
|
23
20
|
"README.md",
|
|
24
21
|
"AGENTS.md"
|
|
25
22
|
],
|
|
@@ -33,7 +30,6 @@
|
|
|
33
30
|
"docs:check": "node scripts/check-docs.mjs",
|
|
34
31
|
"init:check": "node scripts/check-init.mjs",
|
|
35
32
|
"update:check": "node scripts/check-update.mjs",
|
|
36
|
-
"eval": "node evals/run.mjs",
|
|
37
33
|
"lint": "biome check ."
|
|
38
34
|
},
|
|
39
35
|
"devDependencies": {
|
|
@@ -41333,20 +41333,6 @@ Pass --force to scaffold into it anyway (existing files with the same name are o
|
|
|
41333
41333
|
}
|
|
41334
41334
|
})() : null;
|
|
41335
41335
|
copyDirSync(templateSrc, dest);
|
|
41336
|
-
const pkgRoot = packageRoot();
|
|
41337
|
-
const examplesPermSrc = import_node_path8.default.join(pkgRoot, "examples", "permissions");
|
|
41338
|
-
if (import_node_fs10.default.existsSync(examplesPermSrc)) {
|
|
41339
|
-
copyDirSync(examplesPermSrc, import_node_path8.default.join(dest, "examples", "permissions"));
|
|
41340
|
-
}
|
|
41341
|
-
const customMandateSrc = import_node_path8.default.join(pkgRoot, "examples", "custom-mandate");
|
|
41342
|
-
if (import_node_fs10.default.existsSync(customMandateSrc)) {
|
|
41343
|
-
copyDirSync(customMandateSrc, import_node_path8.default.join(dest, "examples", "custom-mandate"));
|
|
41344
|
-
}
|
|
41345
|
-
const permModelSrc = import_node_path8.default.join(pkgRoot, "docs", "PERMISSION_MODEL.md");
|
|
41346
|
-
if (import_node_fs10.default.existsSync(permModelSrc)) {
|
|
41347
|
-
import_node_fs10.default.mkdirSync(import_node_path8.default.join(dest, "docs"), { recursive: true });
|
|
41348
|
-
writeIfMissing2(import_node_path8.default.join(dest, "docs", "PERMISSION_MODEL.md"), import_node_fs10.default.readFileSync(permModelSrc, "utf-8"));
|
|
41349
|
-
}
|
|
41350
41336
|
const pkgPath = import_node_path8.default.join(dest, "package.json");
|
|
41351
41337
|
if (import_node_fs10.default.existsSync(pkgPath)) {
|
|
41352
41338
|
const pkg = JSON.parse(import_node_fs10.default.readFileSync(pkgPath, "utf-8"));
|
|
@@ -7,7 +7,8 @@ import { type Address, type PublicClient } from "viem";
|
|
|
7
7
|
*
|
|
8
8
|
* This is THE single source of truth for the registration fee. The kernel
|
|
9
9
|
* charges `fee × N` for N permissions, bounded by MAX_PERMISSION_FEE_WEI
|
|
10
|
-
* (0.
|
|
10
|
+
* (0.01 ETH — the constitutional cap in SailGovernance; the seeded launch
|
|
11
|
+
* default fee is 0.001 ETH) and refunding any excess; there is NO bytecode/size-based
|
|
11
12
|
* component in the live contracts (that "variable" formula existed only in
|
|
12
13
|
* stale protocol docs describing an abandoned design). Underpaying reverts with
|
|
13
14
|
* InsufficientFee(required, provided).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fees.d.ts","sourceRoot":"","sources":["../src/fees.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,YAAY,EAAe,MAAM,MAAM,CAAC;AAGpE
|
|
1
|
+
{"version":3,"file":"fees.d.ts","sourceRoot":"","sources":["../src/fees.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,YAAY,EAAe,MAAM,MAAM,CAAC;AAGpE;;;;;;;;;;;;;GAaG;AACH,wBAAsB,6BAA6B,CACjD,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,OAAO,GAClB,OAAO,CAAC,MAAM,CAAC,CAMjB;AAED,mEAAmE;AACnE,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,mFAAmF;AACnF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,aAAa,EAAE,aAAa,EAAE,CAAC;CAChC,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAsB,8BAA8B,CAClD,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,OAAO,EACnB,WAAW,EAAE,OAAO,EAAE,GACrB,OAAO,CAAC,kBAAkB,CAAC,CAK7B;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAMvE;AAED;iFACiF;AACjF,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,gCAAgC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,sCAAsC;IACtC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAChB,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;CAMrE;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CASjF"}
|
|
@@ -8,7 +8,8 @@ import { SailGovernanceAbi } from "./abis/SailGovernance.js";
|
|
|
8
8
|
*
|
|
9
9
|
* This is THE single source of truth for the registration fee. The kernel
|
|
10
10
|
* charges `fee × N` for N permissions, bounded by MAX_PERMISSION_FEE_WEI
|
|
11
|
-
* (0.
|
|
11
|
+
* (0.01 ETH — the constitutional cap in SailGovernance; the seeded launch
|
|
12
|
+
* default fee is 0.001 ETH) and refunding any excess; there is NO bytecode/size-based
|
|
12
13
|
* component in the live contracts (that "variable" formula existed only in
|
|
13
14
|
* stale protocol docs describing an abandoned design). Underpaying reverts with
|
|
14
15
|
* InsufficientFee(required, provided).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fees.js","sourceRoot":"","sources":["../src/fees.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,WAAW,EAAE,MAAM,MAAM,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7D
|
|
1
|
+
{"version":3,"file":"fees.js","sourceRoot":"","sources":["../src/fees.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,WAAW,EAAE,MAAM,MAAM,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7D;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B,CACjD,YAA0B,EAC1B,UAAmB;IAEnB,OAAO,YAAY,CAAC,YAAY,CAAC;QAC/B,OAAO,EAAE,UAAU;QACnB,GAAG,EAAE,iBAAiB;QACtB,YAAY,EAAE,2BAA2B;KAC1C,CAAoB,CAAC;AACxB,CAAC;AAgBD;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAClD,YAA0B,EAC1B,UAAmB,EACnB,WAAsB;IAEtB,MAAM,UAAU,GAAG,MAAM,6BAA6B,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;IACjF,MAAM,aAAa,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;IAC5F,MAAM,QAAQ,GAAG,UAAU,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IACzD,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAA4B;IAC7D,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC;IAC5C,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,0DAA0D,CAAC;IACnF,MAAM,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,aAAa,CAAC;IACxD,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,wCAAwC;IAC5F,OAAO,qBAAqB,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,SAAS,KAAK,IAAI,IAAI,MAAM,WAAW,CAAC,SAAS,CAAC,OAAO,CAAC;AACtH,CAAC;AAED;iFACiF;AACjF,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,gCAAgC;IACvB,WAAW,CAAS;IAC7B,sCAAsC;IAC7B,UAAU,CAAS;IAC5B,YAAY,OAAe,EAAE,WAAmB,EAAE,UAAkB;QAClE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,CAAC;CACF;AAED,kFAAkF;AAClF,MAAM,UAAU,YAAY,CAAC,UAAkB,EAAE,WAAmB;IAClE,OAAO,UAAU,IAAI,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,GAAG,UAAU,CAAC;AACnE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAAkB,EAAE,WAAmB;IACzE,IAAI,UAAU,GAAG,WAAW,EAAE,CAAC;QAC7B,MAAM,IAAI,oBAAoB,CAC5B,4BAA4B,WAAW,CAAC,WAAW,CAAC,yBAAyB;YAC3E,qBAAqB,WAAW,CAAC,UAAU,CAAC,OAAO,EACrD,WAAW,EACX,UAAU,CACX,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Do not edit manually — run `pnpm build` to regenerate.
|
|
6
6
|
*
|
|
7
7
|
* Spec version : 1.2.1
|
|
8
|
-
* Generated at : 2026-07-
|
|
8
|
+
* Generated at : 2026-07-02T17:23:23.707Z
|
|
9
9
|
*/
|
|
10
10
|
export declare const SAIL_INTELLIGENCE_BASE_URL = "https://api.sail.money";
|
|
11
11
|
export declare const SAIL_INTELLIGENCE_DOCS_URL = "https://api.sail.money/docs";
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Do not edit manually — run `pnpm build` to regenerate.
|
|
6
6
|
*
|
|
7
7
|
* Spec version : 1.2.1
|
|
8
|
-
* Generated at : 2026-07-
|
|
8
|
+
* Generated at : 2026-07-02T17:23:23.707Z
|
|
9
9
|
*/
|
|
10
10
|
export const SAIL_INTELLIGENCE_BASE_URL = "https://api.sail.money";
|
|
11
11
|
export const SAIL_INTELLIGENCE_DOCS_URL = "https://api.sail.money/docs";
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
],
|
|
53
53
|
"repository": {
|
|
54
54
|
"type": "git",
|
|
55
|
-
"url": "https://github.com/
|
|
55
|
+
"url": "https://github.com/sail-money/Sailor.git",
|
|
56
56
|
"directory": "packages/sdk"
|
|
57
57
|
},
|
|
58
58
|
"homepage": "https://sail.money",
|
|
59
59
|
"bugs": {
|
|
60
|
-
"url": "https://github.com/
|
|
60
|
+
"url": "https://github.com/sail-money/Sailor/issues"
|
|
61
61
|
},
|
|
62
62
|
"engines": {
|
|
63
63
|
"node": ">=18.0.0"
|
|
@@ -15,7 +15,7 @@ Permissions only bound on-chain actions: venues with off-chain order matching (P
|
|
|
15
15
|
| `BoundedStake_Venice_Base.sol` | Venice (VVV) staking · Base | Recipient binding on stake; claim() structurally routes rewards to caller (the SMA) | `0xadc9772e` stake(address,uint256), `0x4e71d92d` claim() |
|
|
16
16
|
| `BoundedTransfer_ERC20_Ethereum.sol` | ERC-20 · any EVM | Token allowlist + recipient allowlist + per-transfer cap; caps are per-token base units (USDC 6 decimals, WETH 18) — one permission per token if amounts differ | `0xa9059cbb` transfer(address,uint256) |
|
|
17
17
|
| `BoundedPerp_GMXv2_Arbitrum.sol` | GMX v2 ExchangeRouter · Arbitrum | Market allowlist + collateral cap + size cap (USD is 1e30-scaled) + long/short flags on a struct-typed call | `0x212234c3` createOrder(CreateOrderParams) — MUST re-verify, see below |
|
|
18
|
-
| `
|
|
18
|
+
| `BoundedLimitless_Base.sol` | Limitless CTF exchange · Base | Condition allowlist + stake cap + outcome allowlist on a prediction market | buy(bytes32,uint256,uint256) — **ABI UNVERIFIED**, see below |
|
|
19
19
|
| `BoundedApproveAndCallBatch.sol` | Sail batch dispatch · any selective kernel | `IBatchPermission`: atomic approve → consume → reset-to-0 in exactly 3 calls; single-call evaluate() deliberately returns false | `0x095ea7b3` approve (calls 0 and 2) |
|
|
20
20
|
|
|
21
21
|
## Lessons the examples encode
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sail-template-swap
|
|
3
|
-
description: Gate an SMA's DEX swaps by REUSING the shared SwapPermission singleton (Protocol/contracts/templates/SwapPermission.sol) — register + configure, no per-SMA deploy. Use for a bounded swap / DCA mandate on Uniswap V3, V3-02, or V2 with router + token-in/out allowlists, a per-tx cap, and a MANDATORY oracle slippage band (priceOracle is required — for no-oracle tokens use sail-template-swap-no-oracle). For the LI.FI aggregator
|
|
3
|
+
description: Gate an SMA's DEX swaps by REUSING the shared SwapPermission singleton (Protocol/contracts/templates/SwapPermission.sol) — register + configure, no per-SMA deploy. Use for a bounded swap / DCA mandate on Uniswap V3, V3-02, or V2 with router + token-in/out allowlists, a per-tx cap, and a MANDATORY oracle slippage band (priceOracle is required — for no-oracle tokens use sail-template-swap-no-oracle). For the LI.FI aggregator or Pendle, author a bespoke permission via sail-mandates. NOTE: `sailor mandate attach` only registers — you must also configure per-account (see steps).
|
|
4
4
|
compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires SwapPermission deployed on the target chain (recorded in sail-templates/deployed.json); run sail-templates first.
|
|
5
5
|
metadata:
|
|
6
6
|
workspace: sailor-harness
|
|
@@ -192,7 +192,7 @@ it is below `amountIn`.
|
|
|
192
192
|
(live-pool hallucination band — catches honest mistakes, NOT MEV/flash-loan attacks) or author a
|
|
193
193
|
bespoke permission. There is no oracle-off mode on `SwapPermission`.
|
|
194
194
|
- **Aggregator (LI.FI) or opaque calldata** → the mandate can't inspect the route; use
|
|
195
|
-
[`sail-
|
|
195
|
+
[`sail-mandates`](../sail-mandates/SKILL.md).
|
|
196
196
|
- **`SwapPermission` not deployed on your chain** → check `deployed.json` first (it's live on all
|
|
197
197
|
11 Sailor-bundled chains as of the current deploy); for anything outside that set, author your
|
|
198
198
|
own via [`sail-mandates`](../sail-mandates/SKILL.md).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sail-template-swap-no-oracle
|
|
3
|
-
description: Gate an SMA's DEX swaps for tokens that have NO oracle by REUSING the shared SwapPermissionNoOracle singleton (Protocol/contracts/templates/SwapPermissionNoOracle.sol) — register + configure, no per-SMA deploy. Use for a bounded swap mandate on Uniswap V3/V3-02/V2 (and forks) where no manipulation-resistant price feed exists: router + token-in/out allowlists, a per-tx cap, recipient pinned to the SMA, and a per-pair live-pool "hallucination band". NOT manipulation-resistant — if the token HAS an oracle use sail-template-swap instead; for the LI.FI aggregator
|
|
3
|
+
description: Gate an SMA's DEX swaps for tokens that have NO oracle by REUSING the shared SwapPermissionNoOracle singleton (Protocol/contracts/templates/SwapPermissionNoOracle.sol) — register + configure, no per-SMA deploy. Use for a bounded swap mandate on Uniswap V3/V3-02/V2 (and forks) where no manipulation-resistant price feed exists: router + token-in/out allowlists, a per-tx cap, recipient pinned to the SMA, and a per-pair live-pool "hallucination band". NOT manipulation-resistant — if the token HAS an oracle use sail-template-swap instead; for the LI.FI aggregator author a bespoke permission via sail-mandates. Deployed on all 11 Sailor-bundled chains (recorded in sail-templates/deployed.json).
|
|
4
4
|
compatibility: A Sailor project (`@sail/sdk`, `sailor` CLI). Requires SwapPermissionNoOracle deployed on the target chain (recorded in sail-templates/deployed.json); run sail-templates first.
|
|
5
5
|
metadata:
|
|
6
6
|
workspace: sailor-harness
|
|
@@ -97,4 +97,4 @@ Template-specific bits:
|
|
|
97
97
|
[`sail-template-swap`](../sail-template-swap/SKILL.md) wherever an oracle exists.
|
|
98
98
|
- `toleranceBps` is capped at 50% in source; a wider band is meaningless and rejected at configure.
|
|
99
99
|
- Unaudited example — step 4 is mandatory.
|
|
100
|
-
- Aggregator routing (opaque calldata) → [`sail-
|
|
100
|
+
- Aggregator routing (opaque calldata) → author a bespoke permission via [`sail-mandates`](../sail-mandates/SKILL.md).
|
|
@@ -124,8 +124,8 @@ Full mechanics (EIP-712 sigs, `configure`/`configureDirect`, `attachBatch`, `rep
|
|
|
124
124
|
|
|
125
125
|
No shared template covers: **perps** (GMX, Gains, Synthetix), **prediction markets** (Azuro,
|
|
126
126
|
Limitless), or the **LI.FI aggregator**. For those, author a bespoke `IPermission` via
|
|
127
|
-
`sailor mandate deploy` — see [`sail-
|
|
128
|
-
|
|
127
|
+
`sailor mandate deploy` — see [`sail-mandates`](../sail-mandates/SKILL.md) for the
|
|
128
|
+
authoring flow and `examples/permissions/` for worked per-protocol patterns.
|
|
129
129
|
|
|
130
130
|
## Notes
|
|
131
131
|
|
|
@@ -89,5 +89,5 @@ Run `sailor doctor` (read-only, no gas, no keys):
|
|
|
89
89
|
- on a conjunctive kernel, **probes each permission for pass-through** and flags any
|
|
90
90
|
that would brick dispatch.
|
|
91
91
|
|
|
92
|
-
See [
|
|
93
|
-
revert failure-mode catalog.
|
|
92
|
+
See [AGENTS.md](../AGENTS.md) (and the skills it routes to under `.agents/skills/`)
|
|
93
|
+
for the operational decision tree and the revert failure-mode catalog.
|
package/docs/PERMISSION_MODEL.md
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# Sail permission model: conjunctive vs selective
|
|
2
|
-
|
|
3
|
-
The deployed SailKernel ships in **two incompatible dispatch models**. Which one a
|
|
4
|
-
chain runs changes how dispatches are signed *and* how permissions must be written.
|
|
5
|
-
Get this wrong and every dispatch reverts with an opaque selector. This is the single
|
|
6
|
-
most important thing to understand before operating an SMA.
|
|
7
|
-
|
|
8
|
-
## TL;DR
|
|
9
|
-
|
|
10
|
-
| | **Conjunctive** (older) | **Selective** (newer) |
|
|
11
|
-
|---|---|---|
|
|
12
|
-
| Chains today (bundled kernels) | None — all bundled kernels moved to selective | Base (8453), Base Sepolia (84532), Arbitrum (42161), Unichain (130) |
|
|
13
|
-
| `dispatch(...)` | `(account, target, value, data, sig, deadline)` — **no `permission`** | `(account, permission, target, value, data, sig, deadline)` |
|
|
14
|
-
| Which permissions are checked | **ALL** registered permissions; **all must return true** | only the **one** permission named in the call |
|
|
15
|
-
| EIP-712 `Dispatch` struct | no `permission` field | includes `permission` |
|
|
16
|
-
| Batch (`dispatchBatch`/`previewBatch`) | **not available** | available |
|
|
17
|
-
| **Permission design rule** | **MUST pass through calls outside its domain** | may return false freely |
|
|
18
|
-
|
|
19
|
-
Don't guess from a version string — **detect it on-chain** (see below).
|
|
20
|
-
|
|
21
|
-
## The conjunctive pass-through rule (the big footgun)
|
|
22
|
-
|
|
23
|
-
On a conjunctive kernel the kernel calls `evaluate(txData, ctx)` on **every**
|
|
24
|
-
registered permission and ANDs the results. A single `false` blocks the whole
|
|
25
|
-
dispatch (`PermissionDenied`).
|
|
26
|
-
|
|
27
|
-
So a permission that only cares about, say, approvals **must still return `true` for
|
|
28
|
-
every call it doesn't govern**:
|
|
29
|
-
|
|
30
|
-
```solidity
|
|
31
|
-
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
|
|
32
|
-
// Pass through calls outside this permission's domain (conjunctive model).
|
|
33
|
-
if (ctx.selector != APPROVE) return true; // <-- without this line, this
|
|
34
|
-
// permission bricks swaps,
|
|
35
|
-
// transfers, everything.
|
|
36
|
-
// ...domain-specific checks for approve...
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
A permission that returns `false` (or reverts, or runs out of gas — both treated as
|
|
41
|
-
`false`) on unrelated calls **bricks the entire account**: no dispatch of any kind
|
|
42
|
-
can pass. We hit exactly this during bring-up with permissions that "blocked each
|
|
43
|
-
other." The fix was to redeploy every permission with pass-through semantics.
|
|
44
|
-
|
|
45
|
-
Corollary: on a conjunctive kernel you **cannot** have two permissions that each
|
|
46
|
-
enforce a different token's approve — each would reject the other's token. To support
|
|
47
|
-
approving both DAI and USDC you need **one** approve permission that allows both (see
|
|
48
|
-
`examples/lifi-permissions/`), not two narrow ones.
|
|
49
|
-
|
|
50
|
-
Selective kernels don't have this problem: each dispatch names one permission and
|
|
51
|
-
only that one is consulted.
|
|
52
|
-
|
|
53
|
-
## Detect the model on-chain
|
|
54
|
-
|
|
55
|
-
The SDK reads each kernel's public `DISPATCH_TYPEHASH` constant and matches it
|
|
56
|
-
against the canonical hashes for each model. Never assume.
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
const caps = await client.capabilities();
|
|
60
|
-
// caps.dispatchModel: "conjunctive" | "selective"
|
|
61
|
-
// caps.dispatchTypehash, caps.source ("onchain-typehash" | "static-hint")
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Verified typehashes:
|
|
65
|
-
|
|
66
|
-
- conjunctive `DISPATCH_TYPEHASH` = `0x7510c80e…`
|
|
67
|
-
`Dispatch(address account,address target,uint256 value,bytes32 dataHash,uint256 nonce,uint256 deadline)`
|
|
68
|
-
- selective `DISPATCH_TYPEHASH` =
|
|
69
|
-
`Dispatch(address account,address permission,address target,uint256 value,bytes32 dataHash,uint256 nonce,uint256 deadline)`
|
|
70
|
-
|
|
71
|
-
`client.dispatch.single(...)` already signs the correct struct and uses the correct
|
|
72
|
-
ABI for the detected model — you don't sign by hand. `client.dispatch.batch` /
|
|
73
|
-
`preview` throw a clear error on conjunctive kernels (no `dispatchBatch`).
|
|
74
|
-
|
|
75
|
-
## Roles (unchanged across models)
|
|
76
|
-
|
|
77
|
-
| Role | Authority |
|
|
78
|
-
|------|-----------|
|
|
79
|
-
| **Owner** | Holds the Safe; custody anchor. |
|
|
80
|
-
| **Permission Signer** | Authorizes which `IPermission` contracts apply (EIP-712 `RegisterPermissions` / `RevokePermissions`). Signed in the browser signing station — the agent never holds this key. |
|
|
81
|
-
| **Manager** | Executes dispatches within the registered permissions (ECDSA / ERC-1271). The agent's hot key. |
|
|
82
|
-
|
|
83
|
-
## Preflight before spending gas
|
|
84
|
-
|
|
85
|
-
Run `sailor doctor` (read-only, no gas, no keys):
|
|
86
|
-
|
|
87
|
-
- detects the dispatch model,
|
|
88
|
-
- lists registered permissions,
|
|
89
|
-
- on a conjunctive kernel, **probes each permission for pass-through** and flags any
|
|
90
|
-
that would brick dispatch.
|
|
91
|
-
|
|
92
|
-
See [AGENTS.md](../AGENTS.md) for the operational decision tree and the
|
|
93
|
-
revert failure-mode catalog.
|
package/examples/README.md
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# examples/
|
|
2
|
-
|
|
3
|
-
Reference material for building Sail permission contracts. Nothing here is part of
|
|
4
|
-
Sail Protocol and nothing here is audited.
|
|
5
|
-
|
|
6
|
-
## What's in here
|
|
7
|
-
|
|
8
|
-
### `permissions/`
|
|
9
|
-
|
|
10
|
-
Protocol-specific permission examples (Uniswap, Aave, GMX, Vault, Pendle, and others).
|
|
11
|
-
These are copied into your project by `sailor init` so you have local starting points to
|
|
12
|
-
adapt. They are not a supported or exhaustive library — review them before using.
|
|
13
|
-
|
|
14
|
-
### `custom-mandate/`
|
|
15
|
-
|
|
16
|
-
A standalone Foundry workspace scaffold for authoring your own `IPermission` contract as a
|
|
17
|
-
separate project. Fork it, rename it, and build from `BoundedCallPermission.sol`.
|
|
18
|
-
|
|
19
|
-
### `lifi-permissions/`
|
|
20
|
-
|
|
21
|
-
Reference-only source for the deployed LiFi EIP-1167 clone implementations
|
|
22
|
-
(`LifiBoundedApprovePermissionCloneable`, `LifiDiamondSwapPermissionCloneable`).
|
|
23
|
-
These document live deployed contracts — do not adapt them directly for new projects.
|
|
24
|
-
See the `README.md` inside for deployment addresses and the conjunctive-kernel caveat.
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.26;
|
|
3
|
-
|
|
4
|
-
import {IPermission, Context} from "../../.sail/contracts/interfaces/IPermission.sol";
|
|
5
|
-
import {CloneInitializable} from "../../.sail/contracts/templates/base/CloneInitializable.sol";
|
|
6
|
-
|
|
7
|
-
/// @title LifiBoundedApprovePermissionCloneable
|
|
8
|
-
/// @notice EIP-1167 clone-template approval permission with PER-TOKEN caps. The
|
|
9
|
-
/// logic contract is deployed once and registered in the SDK's
|
|
10
|
-
/// standaloneTemplates; each account clones it via
|
|
11
|
-
/// PermissionFactory.deployAndAttach and configures via initialize().
|
|
12
|
-
///
|
|
13
|
-
/// The manager may ONLY approve the LiFi Diamond, and only on tokens that
|
|
14
|
-
/// have a configured cap, up to that token's cap. Passes through any
|
|
15
|
-
/// non-approve call (conjunctive model).
|
|
16
|
-
///
|
|
17
|
-
/// Caps are PER TOKEN (not one global cap) because token value and decimals
|
|
18
|
-
/// differ — e.g. 1 DAI = 1e18 base units vs 1 USDC = 1e6. A single cap can't
|
|
19
|
-
/// bound both sensibly.
|
|
20
|
-
contract LifiBoundedApprovePermissionCloneable is IPermission, CloneInitializable {
|
|
21
|
-
bytes4 private constant APPROVE = 0x095ea7b3;
|
|
22
|
-
address public constant LIFI_DIAMOND = 0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE;
|
|
23
|
-
|
|
24
|
-
// Per-token approve cap, in the token's base units. 0 = token not allowed.
|
|
25
|
-
// A mapping occupies its own slot pointer and does not pack with the
|
|
26
|
-
// CloneInitializable `_initialized` bool at slot 0.
|
|
27
|
-
mapping(address token => uint256 cap) public maxApprovePerToken;
|
|
28
|
-
address public permissionSigner;
|
|
29
|
-
|
|
30
|
-
error NotPermissionSigner();
|
|
31
|
-
error ZeroAddress();
|
|
32
|
-
error LengthMismatch();
|
|
33
|
-
|
|
34
|
-
modifier onlyPermissionSigner() {
|
|
35
|
-
if (msg.sender != permissionSigner) revert NotPermissionSigner();
|
|
36
|
-
_;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/// @dev Lock the logic contract; only clones (fresh storage) can be initialized.
|
|
40
|
-
constructor() {
|
|
41
|
-
_disableInitializers();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/// @notice One-time per-clone configuration.
|
|
45
|
-
/// @param tokens Tokens the manager may approve to the LiFi Diamond.
|
|
46
|
-
/// @param caps Per-token cap (token base units); index-aligned with `tokens`.
|
|
47
|
-
/// @param _permissionSigner Owner wallet; sole authority for post-init updates.
|
|
48
|
-
function initialize(
|
|
49
|
-
address[] memory tokens,
|
|
50
|
-
uint256[] memory caps,
|
|
51
|
-
address _permissionSigner
|
|
52
|
-
) external initializer {
|
|
53
|
-
if (_permissionSigner == address(0)) revert ZeroAddress();
|
|
54
|
-
if (tokens.length != caps.length) revert LengthMismatch();
|
|
55
|
-
permissionSigner = _permissionSigner;
|
|
56
|
-
for (uint256 i; i < tokens.length; i++) {
|
|
57
|
-
maxApprovePerToken[tokens[i]] = caps[i];
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/// @notice Set (cap > 0) or clear (cap == 0) a token's per-tx approve cap.
|
|
62
|
-
function setTokenCap(address token, uint256 cap) external onlyPermissionSigner {
|
|
63
|
-
maxApprovePerToken[token] = cap;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
|
|
67
|
-
// Pass through calls outside this permission's domain (conjunctive model).
|
|
68
|
-
if (ctx.selector != APPROVE) return true;
|
|
69
|
-
if (ctx.target == address(0)) return false; // token is ctx.target
|
|
70
|
-
if (txData.length < 68) return false;
|
|
71
|
-
|
|
72
|
-
(address spender, uint256 amount) = abi.decode(txData[4:], (address, uint256));
|
|
73
|
-
if (spender != LIFI_DIAMOND) return false;
|
|
74
|
-
|
|
75
|
-
uint256 cap = maxApprovePerToken[ctx.target];
|
|
76
|
-
if (cap == 0) return false; // token not allowed
|
|
77
|
-
if (amount > cap) return false; // over the per-token cap
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function discriminator() external pure returns (bytes32) {
|
|
82
|
-
return keccak256("LifiBoundedApprovePermissionCloneable.v1");
|
|
83
|
-
}
|
|
84
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.26;
|
|
3
|
-
|
|
4
|
-
import {IPermission, Context} from "../../.sail/contracts/interfaces/IPermission.sol";
|
|
5
|
-
import {CloneInitializable} from "../../.sail/contracts/templates/base/CloneInitializable.sol";
|
|
6
|
-
|
|
7
|
-
/// @title LifiDiamondSwapPermissionCloneable
|
|
8
|
-
/// @notice EIP-1167 clone-template version of LifiDiamondSwapPermission. The logic
|
|
9
|
-
/// contract is deployed once and registered in the SDK's standaloneTemplates;
|
|
10
|
-
/// each account gets its own clone via PermissionFactory.deployAndAttach,
|
|
11
|
-
/// configured through initialize() (NOT the constructor).
|
|
12
|
-
///
|
|
13
|
-
/// Restricts manager-initiated swaps to the official LiFi Diamond on Base:
|
|
14
|
-
/// - target must be the LiFi Diamond,
|
|
15
|
-
/// - selector must be allowlisted,
|
|
16
|
-
/// - receiver embedded in the calldata must equal ctx.account (the SMA),
|
|
17
|
-
/// - the minAmount field must not exceed the configured cap.
|
|
18
|
-
/// Passes through any call whose target is not the diamond (conjunctive model).
|
|
19
|
-
///
|
|
20
|
-
/// Calldata layout (validated against live Base quotes):
|
|
21
|
-
/// selector(4) + word0(32) + word1(32) + word2(32) + receiver(32) + minAmount(32)
|
|
22
|
-
/// → receiver at offset 100, minAmount at offset 132.
|
|
23
|
-
contract LifiDiamondSwapPermissionCloneable is IPermission, CloneInitializable {
|
|
24
|
-
// Official LiFi Diamond on Base Mainnet.
|
|
25
|
-
address public constant LIFI_DIAMOND = 0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE;
|
|
26
|
-
|
|
27
|
-
// Storage starts after CloneInitializable's `_initialized` bool (slot 0). A
|
|
28
|
-
// mapping occupies its own slot pointer and does not pack with the bool.
|
|
29
|
-
mapping(bytes4 selector => bool) public isAllowedSelector;
|
|
30
|
-
uint256 public maxMinAmountPerTx;
|
|
31
|
-
address public permissionSigner;
|
|
32
|
-
|
|
33
|
-
error NotPermissionSigner();
|
|
34
|
-
error ZeroAddress();
|
|
35
|
-
|
|
36
|
-
modifier onlyPermissionSigner() {
|
|
37
|
-
if (msg.sender != permissionSigner) revert NotPermissionSigner();
|
|
38
|
-
_;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/// @dev Lock the logic contract; only clones (fresh storage) can be initialized.
|
|
42
|
-
constructor() {
|
|
43
|
-
_disableInitializers();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/// @notice One-time per-clone configuration.
|
|
47
|
-
/// @param allowedSelectors LiFi Diamond selectors to allowlist on init.
|
|
48
|
-
/// @param _maxMinAmountPerTx Cap on the minAmount field (type(uint256).max = uncapped).
|
|
49
|
-
/// @param _permissionSigner Owner wallet; sole authority for post-init updates.
|
|
50
|
-
function initialize(
|
|
51
|
-
bytes4[] memory allowedSelectors,
|
|
52
|
-
uint256 _maxMinAmountPerTx,
|
|
53
|
-
address _permissionSigner
|
|
54
|
-
) external initializer {
|
|
55
|
-
if (_permissionSigner == address(0)) revert ZeroAddress();
|
|
56
|
-
maxMinAmountPerTx = _maxMinAmountPerTx;
|
|
57
|
-
permissionSigner = _permissionSigner;
|
|
58
|
-
for (uint256 i; i < allowedSelectors.length; i++) {
|
|
59
|
-
isAllowedSelector[allowedSelectors[i]] = true;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function setMaxMinAmountPerTx(uint256 newMax) external onlyPermissionSigner {
|
|
64
|
-
maxMinAmountPerTx = newMax;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/// @notice Add a LiFi selector. Only after verifying its calldata places
|
|
68
|
-
/// `receiver` at offset 100 and `minAmount` at offset 132.
|
|
69
|
-
function addSelector(bytes4 selector) external onlyPermissionSigner {
|
|
70
|
-
isAllowedSelector[selector] = true;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function removeSelector(bytes4 selector) external onlyPermissionSigner {
|
|
74
|
-
isAllowedSelector[selector] = false;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
uint256 private constant RECEIVER_OFFSET = 100;
|
|
78
|
-
uint256 private constant MIN_DATA_LEN = 164;
|
|
79
|
-
|
|
80
|
-
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
|
|
81
|
-
// Pass through calls outside this permission's domain (conjunctive model).
|
|
82
|
-
if (ctx.target != LIFI_DIAMOND) return true;
|
|
83
|
-
if (!isAllowedSelector[ctx.selector]) return false;
|
|
84
|
-
if (txData.length < MIN_DATA_LEN) return false;
|
|
85
|
-
|
|
86
|
-
address receiver = abi.decode(txData[RECEIVER_OFFSET:RECEIVER_OFFSET + 32], (address));
|
|
87
|
-
uint256 minAmount = abi.decode(txData[RECEIVER_OFFSET + 32:RECEIVER_OFFSET + 64], (uint256));
|
|
88
|
-
|
|
89
|
-
if (receiver != ctx.account) return false;
|
|
90
|
-
if (minAmount > maxMinAmountPerTx) return false;
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function discriminator() external pure returns (bytes32) {
|
|
95
|
-
return keccak256("LifiDiamondSwapPermissionCloneable.v1");
|
|
96
|
-
}
|
|
97
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# LiFi clone-permission templates
|
|
2
|
-
|
|
3
|
-
Canonical source (for reference) of the EIP-1167 **clone** permission templates
|
|
4
|
-
used for LiFi-based swaps/DCA. The logic contracts are deployed once per chain and
|
|
5
|
-
registered in the SDK deployment registry
|
|
6
|
-
(`packages/sdk/src/deployments.ts` → `standaloneTemplates` / `cloneTemplates`).
|
|
7
|
-
Each account gets its own clone via `PermissionFactory.deployAndAttach`, configured
|
|
8
|
-
through `initialize()` (never the constructor).
|
|
9
|
-
|
|
10
|
-
These follow the `CloneInitializable` pattern: the constructor calls
|
|
11
|
-
`_disableInitializers()` to permanently lock the logic contract, and a one-time
|
|
12
|
-
`initialize()` (guarded by the `initializer` modifier) configures each clone.
|
|
13
|
-
|
|
14
|
-
> The `import` paths reference `../../.sail/contracts/{interfaces,templates/base}`
|
|
15
|
-
> from the Foundry project they were built in
|
|
16
|
-
> (`tests/base-mainnet-agent-01/`). This folder is **reference source** — sailor has
|
|
17
|
-
> no Foundry build. Build/deploy happens in that project via
|
|
18
|
-
> `scripts/deploy-clone-templates.ts`.
|
|
19
|
-
|
|
20
|
-
> **Note:** The kernels bundled in `@sail.money/sailor/sdk` (Base, Base Sepolia, Arbitrum, Unichain) now all run the **selective** dispatch model — verified on-chain against each kernel's `DISPATCH_TYPEHASH`. These templates were written for the older conjunctive model and include pass-through logic (`return true` for calls outside their domain) that is not required on selective kernels. Review before deploying against a selective kernel; the pass-through logic is harmless but unnecessary.
|
|
21
|
-
|
|
22
|
-
## Contracts
|
|
23
|
-
|
|
24
|
-
### LifiDiamondSwapPermissionCloneable → `boundedLiFi`
|
|
25
|
-
Restricts manager swaps to the official LiFi Diamond:
|
|
26
|
-
target == diamond, selector allowlisted, embedded receiver == `ctx.account`,
|
|
27
|
-
`minAmount <= maxMinAmountPerTx`. Passes through any call whose target is not the
|
|
28
|
-
diamond (conjunctive-kernel rule).
|
|
29
|
-
|
|
30
|
-
`initialize(bytes4[] allowedSelectors, uint256 maxMinAmountPerTx, address permissionSigner)`
|
|
31
|
-
|
|
32
|
-
### LifiBoundedApprovePermissionCloneable → `boundedApprove`
|
|
33
|
-
Approve only the LiFi Diamond, only on tokens with a configured cap, up to that
|
|
34
|
-
cap. **Per-token caps** (`mapping(token => cap)`) because token value/decimals
|
|
35
|
-
differ (1 DAI = 1e18 vs 1 USDC = 1e6). Passes through non-approve calls.
|
|
36
|
-
|
|
37
|
-
`initialize(address[] tokens, uint256[] caps, address permissionSigner)`
|
|
38
|
-
|
|
39
|
-
## Deployed logic addresses
|
|
40
|
-
|
|
41
|
-
| Template | Chain | Address |
|
|
42
|
-
|---|---|---|
|
|
43
|
-
| boundedLiFi | Base mainnet (8453) | `0xF1abcF774250fD1A8147B56DA07Bf9021064650A` |
|
|
44
|
-
| boundedApprove | Base mainnet (8453) | `0x9c0b86daf9e75d759a5D165aD7366e52b3353fD8` |
|
|
45
|
-
|
|
46
|
-
Both verified `initialized() == true` (logic locked) post-deploy.
|
|
47
|
-
|
|
48
|
-
## Conjunctive-kernel note
|
|
49
|
-
|
|
50
|
-
Both Base kernels use the **conjunctive** dispatch model: every registered
|
|
51
|
-
permission is evaluated and ALL must return true. So a permission MUST pass through
|
|
52
|
-
calls outside its own domain (return `true`), or it bricks unrelated dispatches.
|
|
53
|
-
Both templates above do this.
|