diff --git a/packages/smart-accounts-kit/src/caveatBuilder/coreCaveatBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/coreCaveatBuilder.ts index 963e2ce..36f05ba 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/coreCaveatBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/coreCaveatBuilder.ts @@ -83,12 +83,13 @@ import { } from './specificActionERC20TransferBatchBuilder'; import { timestamp, timestampBuilder } from './timestampBuilder'; import { valueLte, valueLteBuilder } from './valueLteBuilder'; +import type { CaveatType } from '../constants'; // While we could derive CoreCaveatMap from the createCaveatBuilder function, // doing so would significantly complicate type resolution. By explicitly // declaring the return type of createCaveatBuilder, we ensure the caveat // map remains synchronized with the actual implementation. -type CoreCaveatMap = { +type CoreCaveatMapByString = { allowedMethods: typeof allowedMethodsBuilder; allowedTargets: typeof allowedTargetsBuilder; deployed: typeof deployedBuilder; @@ -122,6 +123,10 @@ type CoreCaveatMap = { ownershipTransfer: typeof ownershipTransferBuilder; }; +type CoreCaveatMap = CoreCaveatMapByString & { + [K in CaveatType as `${K}`]: CoreCaveatMapByString[`${K}`]; +}; + /** * A caveat builder type that includes all core caveat types pre-configured. * This type represents a fully configured caveat builder with all the standard diff --git a/packages/smart-accounts-kit/src/constants.ts b/packages/smart-accounts-kit/src/constants.ts index 11e9a2c..43a66e8 100644 --- a/packages/smart-accounts-kit/src/constants.ts +++ b/packages/smart-accounts-kit/src/constants.ts @@ -53,3 +53,42 @@ export enum ScopeType { OwnershipTransfer = 'ownershipTransfer', FunctionCall = 'functionCall', } + +/** + * Caveat types for enforcer functions used in delegations. + * Can be used when defining caveats either via CaveatBuilder.addCaveat + * or in the caveats array in createDelegation. + */ +export enum CaveatType { + AllowedMethods = 'allowedMethods', + AllowedTargets = 'allowedTargets', + Deployed = 'deployed', + AllowedCalldata = 'allowedCalldata', + Erc20BalanceChange = 'erc20BalanceChange', + Erc721BalanceChange = 'erc721BalanceChange', + Erc1155BalanceChange = 'erc1155BalanceChange', + ValueLte = 'valueLte', + LimitedCalls = 'limitedCalls', + Id = 'id', + Nonce = 'nonce', + Timestamp = 'timestamp', + BlockNumber = 'blockNumber', + Erc20TransferAmount = 'erc20TransferAmount', + Erc20Streaming = 'erc20Streaming', + NativeTokenStreaming = 'nativeTokenStreaming', + Erc721Transfer = 'erc721Transfer', + NativeTokenTransferAmount = 'nativeTokenTransferAmount', + NativeBalanceChange = 'nativeBalanceChange', + Redeemer = 'redeemer', + NativeTokenPayment = 'nativeTokenPayment', + ArgsEqualityCheck = 'argsEqualityCheck', + SpecificActionERC20TransferBatch = 'specificActionERC20TransferBatch', + Erc20PeriodTransfer = 'erc20PeriodTransfer', + NativeTokenPeriodTransfer = 'nativeTokenPeriodTransfer', + ExactCalldataBatch = 'exactCalldataBatch', + ExactCalldata = 'exactCalldata', + ExactExecution = 'exactExecution', + ExactExecutionBatch = 'exactExecutionBatch', + MultiTokenPeriod = 'multiTokenPeriod', + OwnershipTransfer = 'ownershipTransfer', +} diff --git a/packages/smart-accounts-kit/src/index.ts b/packages/smart-accounts-kit/src/index.ts index 18aeab8..5ac166f 100644 --- a/packages/smart-accounts-kit/src/index.ts +++ b/packages/smart-accounts-kit/src/index.ts @@ -37,7 +37,12 @@ export { getSmartAccountsEnvironment, } from './smartAccountsEnvironment'; -export { Implementation, TransferWindow, ScopeType } from './constants'; +export { + Implementation, + TransferWindow, + ScopeType, + CaveatType, +} from './constants'; export { createExecution, ExecutionMode } from './executions'; diff --git a/packages/smart-accounts-kit/test/caveatBuilder/createCaveatBuilder.test.ts b/packages/smart-accounts-kit/test/caveatBuilder/createCaveatBuilder.test.ts index 7006e38..a37816e 100644 --- a/packages/smart-accounts-kit/test/caveatBuilder/createCaveatBuilder.test.ts +++ b/packages/smart-accounts-kit/test/caveatBuilder/createCaveatBuilder.test.ts @@ -4,6 +4,7 @@ import { expect, describe, it } from 'vitest'; import { createCaveatBuilder, CaveatBuilder } from '../../src/caveatBuilder'; import { BalanceChangeType } from '../../src/caveatBuilder/types'; +import { CaveatType } from '../../src/constants'; import type { SmartAccountsEnvironment } from '../../src/types'; import { randomAddress, randomBytes } from '../utils'; @@ -489,4 +490,436 @@ describe('createCaveatBuilder()', () => { expect(isAddress(caveat.enforcer)).to.equal(true); }); + + describe('caveat builders using CaveatType enum', () => { + it('should add allowedMethods caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const selectors = [randomBytes(4), randomBytes(4)]; + + const caveats = builder + .addCaveat(CaveatType.AllowedMethods, { selectors }) + .build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.AllowedMethodsEnforcer, + terms: concat(selectors), + args: '0x00', + }, + ]); + + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add allowedTargets caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const targets: [Address, Address] = [randomAddress(), randomAddress()]; + + const caveats = builder + .addCaveat(CaveatType.AllowedTargets, { targets }) + .build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.AllowedTargetsEnforcer, + terms: targets[0] + targets[1]?.slice(2), + args: '0x00', + }, + ]); + + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add deployed caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const contractAddress = randomAddress(); + const salt = randomBytes(32); + const bytecode = randomBytes(256); + + const caveats = builder + .addCaveat(CaveatType.Deployed, { contractAddress, salt, bytecode }) + .build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.DeployedEnforcer, + terms: concat([contractAddress, pad(salt, { size: 32 }), bytecode]), + args: '0x00', + }, + ]); + + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add allowedCalldata caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const value = randomBytes(128); + const startIndex = Math.floor(Math.random() * 2 ** 32); + + const caveats = builder + .addCaveat(CaveatType.AllowedCalldata, { startIndex, value }) + .build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.AllowedCalldataEnforcer, + terms: concat([toHex(startIndex, { size: 32 }), value]), + args: '0x00', + }, + ]); + + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add erc20BalanceChange caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const tokenAddress = randomAddress(); + const recipient = randomAddress(); + const balance = BigInt( + Math.floor(Math.random() * Number.MAX_SAFE_INTEGER), + ); + + const caveats = builder + .addCaveat(CaveatType.Erc20BalanceChange, { + tokenAddress, + recipient, + balance, + changeType: BalanceChangeType.Increase, + }) + .build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.ERC20BalanceChangeEnforcer, + terms: encodePacked( + ['uint8', 'address', 'address', 'uint256'], + [BalanceChangeType.Increase, tokenAddress, recipient, balance], + ), + args: '0x00', + }, + ]); + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add valueLte caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const maxValue = BigInt( + Math.floor(Math.random() * Number.MAX_SAFE_INTEGER), + ); + + const caveats = builder + .addCaveat(CaveatType.ValueLte, { maxValue }) + .build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.ValueLteEnforcer, + terms: toHex(maxValue, { size: 32 }), + args: '0x00', + }, + ]); + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add limitedCalls caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const limit = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); + + const caveats = builder + .addCaveat(CaveatType.LimitedCalls, { limit }) + .build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.LimitedCallsEnforcer, + terms: toHex(limit, { size: 32 }), + args: '0x00', + }, + ]); + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add id caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const idValue = BigInt(Math.floor(Math.random() * 2 ** 32)); + + const caveats = builder.addCaveat(CaveatType.Id, { id: idValue }).build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.IdEnforcer, + terms: toHex(idValue, { size: 32 }), + args: '0x00', + }, + ]); + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add nonce caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const nonce = randomBytes(16); + + const caveats = builder.addCaveat(CaveatType.Nonce, { nonce }).build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.NonceEnforcer, + terms: pad(nonce, { size: 32 }), + args: '0x00', + }, + ]); + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add timestamp caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const afterThreshold = 1000; + const beforeThreshold = 2000; + + const caveats = builder + .addCaveat(CaveatType.Timestamp, { + afterThreshold, + beforeThreshold, + }) + .build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.TimestampEnforcer, + terms: concat([ + toHex(afterThreshold, { size: 16 }), + toHex(beforeThreshold, { size: 16 }), + ]), + args: '0x00', + }, + ]); + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add blockNumber caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const afterThreshold = 1000n; + const beforeThreshold = 2000n; + + const caveats = builder + .addCaveat(CaveatType.BlockNumber, { + afterThreshold, + beforeThreshold, + }) + .build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.BlockNumberEnforcer, + terms: concat([ + toHex(afterThreshold, { size: 16 }), + toHex(beforeThreshold, { size: 16 }), + ]), + args: '0x00', + }, + ]); + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add nativeTokenTransferAmount caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const maxAmount = 1000000000000000000n; // 1 ETH in wei + + const caveats = builder + .addCaveat(CaveatType.NativeTokenTransferAmount, { maxAmount }) + .build(); + + expect(caveats).to.deep.equal([ + { + enforcer: + environment.caveatEnforcers.NativeTokenTransferAmountEnforcer, + terms: toHex(maxAmount, { size: 32 }), + args: '0x00', + }, + ]); + + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add nativeBalanceChange caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const recipient = randomAddress(); + const minBalance = 500000000000000000n; // 0.5 ETH in wei + + const caveats = builder + .addCaveat(CaveatType.NativeBalanceChange, { + recipient, + balance: minBalance, + changeType: BalanceChangeType.Increase, + }) + .build(); + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.NativeBalanceChangeEnforcer, + terms: encodePacked( + ['uint8', 'address', 'uint256'], + [BalanceChangeType.Increase, recipient, minBalance], + ), + args: '0x00', + }, + ]); + + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add nativeTokenPayment caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const amount = 1000000000000000000n; // 1 ETH in wei + const recipient = randomAddress('lowercase'); + + const caveats = builder + .addCaveat(CaveatType.NativeTokenPayment, { recipient, amount }) + .build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.NativeTokenPaymentEnforcer, + terms: concat([recipient, toHex(amount, { size: 32 })]), + args: '0x00', + }, + ]); + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add erc20TransferAmount caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const tokenAddress = randomAddress(); + const maxAmount = 2000n; + + const caveats = builder + .addCaveat(CaveatType.Erc20TransferAmount, { tokenAddress, maxAmount }) + .build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.ERC20TransferAmountEnforcer, + terms: concat([tokenAddress, toHex(maxAmount, { size: 32 })]), + args: '0x00', + }, + ]); + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add redeemer caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const redeemerAddress = randomAddress(); + + const caveats = builder + .addCaveat(CaveatType.Redeemer, { redeemers: [redeemerAddress] }) + .build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.RedeemerEnforcer, + terms: redeemerAddress, + args: '0x00', + }, + ]); + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + + it('should add argsEqualityCheck caveat using CaveatType enum', () => { + const builder = createCaveatBuilder(environment); + const args = '0x1234567890'; + + const caveats = builder + .addCaveat(CaveatType.ArgsEqualityCheck, { args }) + .build(); + + expect(caveats).to.deep.equal([ + { + enforcer: environment.caveatEnforcers.ArgsEqualityCheckEnforcer, + terms: args, + args: '0x00', + }, + ]); + const caveat = caveats[0]; + if (!caveat) { + throw new Error('caveat is not set'); + } + + expect(isAddress(caveat.enforcer)).to.equal(true); + }); + }); });