@dev.sail.money/sailor 1.3.0-226 → 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.
Files changed (48) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +59 -498
  3. package/package.json +1 -5
  4. package/packages/cli/dist/index.cjs +53 -22
  5. package/packages/sdk/dist/fees.d.ts +2 -1
  6. package/packages/sdk/dist/fees.d.ts.map +1 -1
  7. package/packages/sdk/dist/fees.js +2 -1
  8. package/packages/sdk/dist/fees.js.map +1 -1
  9. package/packages/sdk/dist/intelligence.d.ts +1 -1
  10. package/packages/sdk/dist/intelligence.js +1 -1
  11. package/packages/sdk/package.json +2 -2
  12. package/templates/default/.agents/skills/sail-mandates/references/examples-index.md +3 -3
  13. package/templates/default/.agents/skills/sail-template-swap/SKILL.md +2 -2
  14. package/templates/default/.agents/skills/sail-template-swap-no-oracle/SKILL.md +2 -2
  15. package/templates/default/.agents/skills/sail-templates/SKILL.md +2 -2
  16. package/templates/default/docs/PERMISSION_MODEL.md +2 -2
  17. package/{examples → templates/default/examples}/custom-mandate/.sail/contracts/interfaces/IPermission.sol +4 -0
  18. package/{examples → templates/default/examples}/custom-mandate/README.md +4 -1
  19. package/{examples → templates/default/examples}/permissions/interfaces/IBatchPermission.sol +2 -0
  20. package/{examples → templates/default/examples}/permissions/interfaces/IPermission.sol +4 -0
  21. package/templates/default/test/BoundedCallPermission.t.sol +2 -1
  22. package/docs/PERMISSION_MODEL.md +0 -93
  23. package/examples/README.md +0 -24
  24. package/examples/lifi-permissions/LifiBoundedApprovePermissionCloneable.sol +0 -84
  25. package/examples/lifi-permissions/LifiDiamondSwapPermissionCloneable.sol +0 -97
  26. package/examples/lifi-permissions/README.md +0 -53
  27. package/scripts/check-docs.mjs +0 -309
  28. package/scripts/check-init.mjs +0 -123
  29. package/scripts/check-update.mjs +0 -177
  30. package/scripts/clean.mjs +0 -17
  31. /package/{examples → templates/default/examples}/custom-mandate/foundry.toml +0 -0
  32. /package/{examples → templates/default/examples}/custom-mandate/mandates/BoundedCallPermission.sol +0 -0
  33. /package/{examples → templates/default/examples}/custom-mandate/mandates/README.md +0 -0
  34. /package/{examples → templates/default/examples}/custom-mandate/mandates/SailCalldata.sol +0 -0
  35. /package/{examples → templates/default/examples}/permissions/BoundedApproveAndCallBatch.sol +0 -0
  36. /package/{examples → templates/default/examples}/permissions/BoundedBorrow_AaveV3_Arbitrum.sol +0 -0
  37. /package/{examples → templates/default/examples}/permissions/BoundedLimitless_Base.sol +0 -0
  38. /package/{examples → templates/default/examples}/permissions/BoundedPerp_GMXv2_Arbitrum.sol +0 -0
  39. /package/{examples → templates/default/examples}/permissions/BoundedStake_Venice_Base.sol +0 -0
  40. /package/{examples → templates/default/examples}/permissions/BoundedSupply_AaveV3_Arbitrum.sol +0 -0
  41. /package/{examples → templates/default/examples}/permissions/BoundedSwapNative_UniswapV3_Base.sol +0 -0
  42. /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV3_Base.sol +0 -0
  43. /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV4_Unichain.sol +0 -0
  44. /package/{examples → templates/default/examples}/permissions/BoundedTransfer_ERC20_Ethereum.sol +0 -0
  45. /package/{examples → templates/default/examples}/permissions/BoundedVault_ERC4626_Base.sol +0 -0
  46. /package/{examples → templates/default/examples}/permissions/README.md +0 -0
  47. /package/{examples → templates/default/examples}/permissions/SailCalldata.sol +0 -0
  48. /package/{examples → templates/default/examples}/permissions/foundry.toml +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev.sail.money/sailor",
3
- "version": "1.3.0-226",
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": {
@@ -40545,15 +40545,36 @@ var IPERMISSION_ABI = [
40545
40545
  { name: "selector", type: "bytes4" },
40546
40546
  { name: "value", type: "uint256" },
40547
40547
  { name: "blockTimestamp", type: "uint256" },
40548
- { name: "blockNumber", type: "uint256" }
40548
+ { name: "blockNumber", type: "uint256" },
40549
+ { name: "configEpoch", type: "uint256" }
40549
40550
  ]
40550
40551
  }
40551
40552
  ],
40552
40553
  outputs: [{ type: "bool" }]
40553
40554
  }
40554
40555
  ];
40556
+ var REGISTRATION_EPOCH_ABI2 = [
40557
+ {
40558
+ type: "function",
40559
+ name: "registrationEpoch",
40560
+ stateMutability: "view",
40561
+ inputs: [
40562
+ { name: "account", type: "address" },
40563
+ { name: "permission", type: "address" }
40564
+ ],
40565
+ outputs: [{ type: "uint256" }]
40566
+ }
40567
+ ];
40568
+ async function readRegistrationEpoch(publicClient, kernel, account2, permission) {
40569
+ return await publicClient.readContract({
40570
+ address: kernel,
40571
+ abi: REGISTRATION_EPOCH_ABI2,
40572
+ functionName: "registrationEpoch",
40573
+ args: [account2, permission]
40574
+ });
40575
+ }
40555
40576
  function buildPermissionContext(params) {
40556
- const { account: account2, manager, call: call2, blockInfo } = params;
40577
+ const { account: account2, manager, call: call2, blockInfo, configEpoch } = params;
40557
40578
  const selector = call2.data.length >= 10 ? call2.data.slice(0, 10) : "0x00000000";
40558
40579
  return {
40559
40580
  account: account2,
@@ -40564,12 +40585,23 @@ function buildPermissionContext(params) {
40564
40585
  selector,
40565
40586
  value: call2.value,
40566
40587
  blockTimestamp: blockInfo.timestamp,
40567
- blockNumber: blockInfo.number
40588
+ blockNumber: blockInfo.number,
40589
+ configEpoch
40568
40590
  };
40569
40591
  }
40570
40592
  async function probePermissionForCall(params) {
40571
- const { publicClient, permission, account: account2, manager, call: call2, blockInfo } = params;
40572
- const ctx = buildPermissionContext({ account: account2, manager, call: call2, blockInfo });
40593
+ const { publicClient, kernel, permission, account: account2, manager, call: call2, blockInfo } = params;
40594
+ let configEpoch;
40595
+ try {
40596
+ configEpoch = await readRegistrationEpoch(publicClient, kernel, account2, permission);
40597
+ } catch (err) {
40598
+ return {
40599
+ accepted: false,
40600
+ reverted: true,
40601
+ error: `could not read registrationEpoch from kernel ${kernel} (${err.message.split("\n")[0]})`
40602
+ };
40603
+ }
40604
+ const ctx = buildPermissionContext({ account: account2, manager, call: call2, blockInfo, configEpoch });
40573
40605
  try {
40574
40606
  const accepted = await publicClient.readContract({
40575
40607
  address: permission,
@@ -40583,10 +40615,11 @@ async function probePermissionForCall(params) {
40583
40615
  }
40584
40616
  }
40585
40617
  async function resolvePermissionForCall(params) {
40586
- const { publicClient, account: account2, manager, call: call2, registeredPermissions, blockInfo } = params;
40587
- const ctx = buildPermissionContext({ account: account2, manager, call: call2, blockInfo });
40618
+ const { publicClient, kernel, account: account2, manager, call: call2, registeredPermissions, blockInfo } = params;
40588
40619
  for (const permission of registeredPermissions) {
40589
40620
  try {
40621
+ const configEpoch = await readRegistrationEpoch(publicClient, kernel, account2, permission);
40622
+ const ctx = buildPermissionContext({ account: account2, manager, call: call2, blockInfo, configEpoch });
40590
40623
  const accepted = await publicClient.readContract({
40591
40624
  address: permission,
40592
40625
  abi: IPERMISSION_ABI,
@@ -40656,7 +40689,10 @@ async function probePassThrough(pc, permission, account2) {
40656
40689
  selector: PROBE_SELECTOR,
40657
40690
  value: 0n,
40658
40691
  blockTimestamp: 0n,
40659
- blockNumber: 0n
40692
+ blockNumber: 0n,
40693
+ // Pass-through probes only run on conjunctive kernels, which predate
40694
+ // registrationEpoch — there is no live epoch to read, so probe with 0.
40695
+ configEpoch: 0n
40660
40696
  }
40661
40697
  ]
40662
40698
  });
@@ -40952,6 +40988,10 @@ struct Context {
40952
40988
  uint256 value; // native ETH forwarded (wei)
40953
40989
  uint256 blockTimestamp; // block.timestamp at dispatch
40954
40990
  uint256 blockNumber; // block.number at dispatch
40991
+ uint256 configEpoch; // kernel registrationEpoch(account, permission) at dispatch;
40992
+ // configurable permissions fail closed on a mismatch with the
40993
+ // epoch stamped at configure() time. Custom permissions that
40994
+ // take no post-deploy configuration can ignore it.
40955
40995
  }
40956
40996
 
40957
40997
  /// @title IPermission
@@ -40987,6 +41027,8 @@ struct BatchContext {
40987
41027
  bytes32 batchHash; // keccak256(abi.encode(calls)) \u2014 stable id for the exact sequence
40988
41028
  uint256 blockTimestamp; // block.timestamp at dispatch
40989
41029
  uint256 blockNumber; // block.number at dispatch
41030
+ uint256 configEpoch; // kernel registrationEpoch(account, permission) at dispatch \u2014
41031
+ // same fail-closed freshness check as Context.configEpoch
40990
41032
  }
40991
41033
 
40992
41034
  /// @title IBatchPermission
@@ -41291,20 +41333,6 @@ Pass --force to scaffold into it anyway (existing files with the same name are o
41291
41333
  }
41292
41334
  })() : null;
41293
41335
  copyDirSync(templateSrc, dest);
41294
- const pkgRoot = packageRoot();
41295
- const examplesPermSrc = import_node_path8.default.join(pkgRoot, "examples", "permissions");
41296
- if (import_node_fs10.default.existsSync(examplesPermSrc)) {
41297
- copyDirSync(examplesPermSrc, import_node_path8.default.join(dest, "examples", "permissions"));
41298
- }
41299
- const customMandateSrc = import_node_path8.default.join(pkgRoot, "examples", "custom-mandate");
41300
- if (import_node_fs10.default.existsSync(customMandateSrc)) {
41301
- copyDirSync(customMandateSrc, import_node_path8.default.join(dest, "examples", "custom-mandate"));
41302
- }
41303
- const permModelSrc = import_node_path8.default.join(pkgRoot, "docs", "PERMISSION_MODEL.md");
41304
- if (import_node_fs10.default.existsSync(permModelSrc)) {
41305
- import_node_fs10.default.mkdirSync(import_node_path8.default.join(dest, "docs"), { recursive: true });
41306
- writeIfMissing2(import_node_path8.default.join(dest, "docs", "PERMISSION_MODEL.md"), import_node_fs10.default.readFileSync(permModelSrc, "utf-8"));
41307
- }
41308
41336
  const pkgPath = import_node_path8.default.join(dest, "package.json");
41309
41337
  if (import_node_fs10.default.existsSync(pkgPath)) {
41310
41338
  const pkg = JSON.parse(import_node_fs10.default.readFileSync(pkgPath, "utf-8"));
@@ -44364,6 +44392,7 @@ async function mandateSimulate(options) {
44364
44392
  const [probe, codeCheck] = await Promise.all([
44365
44393
  probePermissionForCall({
44366
44394
  publicClient: pc,
44395
+ kernel: project.contracts.kernel,
44367
44396
  permission,
44368
44397
  account: account2,
44369
44398
  manager,
@@ -45323,6 +45352,8 @@ Configure the chain in the SDK chain registry or set KERNEL_ADDRESS in .sail/.en
45323
45352
  } else {
45324
45353
  permission = await resolvePermissionForCall({
45325
45354
  publicClient,
45355
+ kernel,
45356
+ // narrowed: runCommand validates kernel before runTick runs
45326
45357
  account: accountAddr,
45327
45358
  manager: agentManager.address,
45328
45359
  call: firstCall,
@@ -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.001 ETH) and refunding any excess; there is NO bytecode/size-based
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;;;;;;;;;;;;GAYG;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"}
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.001 ETH) and refunding any excess; there is NO bytecode/size-based
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;;;;;;;;;;;;GAYG;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"}
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-02T15:07:10.282Z
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-02T15:07:10.282Z
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/SailAgent/Sailor.git",
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/SailAgent/Sailor/issues"
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
- | `BoundedBet_Limitless_Base.sol` | Limitless CTF exchange · Base | Condition allowlist + stake cap + outcome allowlist on a prediction market | buy(bytes32,uint256,uint256) — **ABI UNVERIFIED**, see below |
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
@@ -29,6 +29,6 @@ Permissions only bound on-chain actions: venues with off-chain order matching (P
29
29
 
30
30
  ## Interfaces (vendored in `examples/permissions/interfaces/`)
31
31
 
32
- - `IPermission.evaluate(bytes txData, Context ctx) → bool`; Context: `account, manager, submitter, target, selector, value, blockTimestamp, blockNumber`.
33
- - `IBatchPermission.evaluateBatch(Call[] calls, BatchContext ctx) → bool` + `isBatchPermission() → true`; Call: `(target, value, data)`; BatchContext: `account, manager, submitter, permission, batchHash, blockTimestamp, blockNumber`.
32
+ - `IPermission.evaluate(bytes txData, Context ctx) → bool`; Context: `account, manager, submitter, target, selector, value, blockTimestamp, blockNumber, configEpoch`.
33
+ - `IBatchPermission.evaluateBatch(Call[] calls, BatchContext ctx) → bool` + `isBatchPermission() → true`; Call: `(target, value, data)`; BatchContext: `account, manager, submitter, permission, batchHash, blockTimestamp, blockNumber, configEpoch`.
34
34
  - `SailCalldata.sol`: bounded slot-indexed calldata readers (`hasParams`, `asAddress`, `asUint256`, …) — use these instead of `abi.decode` to avoid wrong-offset reads.
@@ -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 use sail-lifi-swap; for Pendle use sail-pendle. NOTE: `sailor mandate attach` only registers — you must also configure per-account (see steps).
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-lifi-swap`](../sail-lifi-swap/SKILL.md) or [`sail-mandates`](../sail-mandates/SKILL.md).
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 use sail-lifi-swap. Deployed on all 11 Sailor-bundled chains (recorded in sail-templates/deployed.json).
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-lifi-swap`](../sail-lifi-swap/SKILL.md).
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-lifi-swap`](../sail-lifi-swap/SKILL.md) for the
128
- custody-bound aggregator pattern and [`sail-pendle`](../sail-pendle/SKILL.md) for Pendle.
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 [AGENT_PLAYBOOK.md](../AGENT_PLAYBOOK.md) for the operational decision tree and the
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.
@@ -10,6 +10,10 @@ struct Context {
10
10
  uint256 value;
11
11
  uint256 blockTimestamp;
12
12
  uint256 blockNumber;
13
+ /// @dev Kernel registrationEpoch(account, permission) at dispatch. Configurable
14
+ /// permissions fail closed on a mismatch with the epoch stamped at configure()
15
+ /// time; permissions without post-deploy configuration can ignore it.
16
+ uint256 configEpoch;
13
17
  }
14
18
 
15
19
  interface IPermission {
@@ -36,11 +36,14 @@ struct Context {
36
36
  uint256 value; // msg.value (native asset) sent with the call
37
37
  uint256 blockTimestamp; // block.timestamp at evaluation
38
38
  uint256 blockNumber; // block.number at evaluation
39
+ uint256 configEpoch; // kernel registrationEpoch(account, permission) at dispatch;
40
+ // ignore it unless your permission takes post-deploy configuration
39
41
  }
40
42
  ```
41
43
 
42
44
  - `evaluate` — your policy. Return `true` to permit the call, `false` to block it. Runs under a
43
- 100k-gas `staticcall`; a revert or gas overage is treated as `false`.
45
+ 150,000-gas `staticcall` (`SailKernel.PERMISSION_GAS_CAP`); a revert or gas overage is treated
46
+ as `false`.
44
47
  - `discriminator` — a stable `bytes32` name for your permission (e.g. `keccak256("MyMandate")`).
45
48
 
46
49
  Keep all policy parameters constructor-configured so each deployment is a complete, reviewable
@@ -26,6 +26,8 @@ struct BatchContext {
26
26
  bytes32 batchHash; // keccak256(abi.encode(calls)) — stable id for the exact sequence
27
27
  uint256 blockTimestamp; // block.timestamp at dispatch
28
28
  uint256 blockNumber; // block.number at dispatch
29
+ uint256 configEpoch; // kernel registrationEpoch(account, permission) at dispatch —
30
+ // same fail-closed freshness check as Context.configEpoch
29
31
  }
30
32
 
31
33
  interface IBatchPermission {
@@ -10,6 +10,10 @@ struct Context {
10
10
  uint256 value;
11
11
  uint256 blockTimestamp;
12
12
  uint256 blockNumber;
13
+ /// @dev Kernel registrationEpoch(account, permission) at dispatch. Configurable
14
+ /// permissions fail closed on a mismatch with the epoch stamped at configure()
15
+ /// time; permissions without post-deploy configuration can ignore it.
16
+ uint256 configEpoch;
13
17
  }
14
18
 
15
19
  interface IPermission {
@@ -39,7 +39,8 @@ contract BoundedCallPermissionTest {
39
39
  selector: selector,
40
40
  value: value,
41
41
  blockTimestamp: block.timestamp,
42
- blockNumber: block.number
42
+ blockNumber: block.number,
43
+ configEpoch: 0 // BoundedCallPermission takes no post-deploy config; value unused
43
44
  });
44
45
  }
45
46
 
@@ -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.
@@ -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
- }