The current implementation uses ArgsEqualityCheckEnforcer which checks if args passed to the enforcer match the terms. This is WRONG. We need:
- ExactCalldataEnforcer — when ALL params are enforced (exact calldata match)
- AllowedCalldataEnforcer — when only SOME params are enforced (one caveat per enforced param)
- ExactCalldataEnforcer:
0x99F2e9bF15ce5eC84685604836F71aB835DBBdED - AllowedCalldataEnforcer:
0xc2b0d624c1c4319760C96503BA27C347F3260f55
Add to BOTH base and baseSepolia:
exactCalldataEnforcer: '0x99F2e9bF15ce5eC84685604836F71aB835DBBdED' as Address,
allowedCalldataEnforcer: '0xc2b0d624c1c4319760C96503BA27C347F3260f55' as Address,Remove argsEqualityCheckEnforcer if it exists.
Add required: boolean to RecipeParam interface:
interface RecipeParam {
name: string
type: string
value: string
locked: boolean // User can't change the value
required: boolean // Value must be filled before granting
description: string
}Update Flaunch recipe:
params: [
{
name: 'recipient',
type: 'address',
value: '',
locked: false,
required: true, // Must fill in recipient
description: 'Address to receive the claimed ETH fees',
},
{
name: 'unwrap',
type: 'bool',
value: 'true',
locked: true,
required: true, // Always true
description: 'Unwrap flETH to native ETH',
},
],Replace the ArgsEqualityCheckEnforcer caveat in buildCustomActionCaveats with the correct enforcer logic:
export function buildCustomActionCaveats(
chainId: number,
targetAddress: Address,
methodSelector: Hex,
customParams: CustomParam[], // Changed from encodedCalldata
maxValueEth: string,
maxCalls?: number,
expiryTimestamp?: number,
): Caveat[] {
const addrs = getAddresses(chainId)
const caveats: Caveat[] = []
// AllowedTargetsEnforcer
caveats.push({
enforcer: addrs.allowedTargetsEnforcer,
terms: encodePacked(['address'], [targetAddress]),
})
// AllowedMethodsEnforcer
caveats.push({
enforcer: addrs.allowedMethodsEnforcer,
terms: encodePacked(['bytes4'], [methodSelector as `0x${string}`]),
})
// Determine which params are enforced
const enforcedParams = customParams.filter(p => p.enforced && p.value)
const allParamsEnforced = enforcedParams.length === customParams.length && customParams.every(p => p.enforced)
if (allParamsEnforced) {
// ALL params enforced → use ExactCalldataEnforcer
// terms = the full calldata (selector + abi-encoded args)
const fullCalldata = encodeFunctionCalldata(methodSelector, customParams)
caveats.push({
enforcer: addrs.exactCalldataEnforcer,
terms: fullCalldata,
})
} else if (enforcedParams.length > 0) {
// SOME params enforced → use AllowedCalldataEnforcer (one per enforced param)
// Each caveat specifies: startIndex (byte offset) and value (expected bytes at that offset)
// ABI encoding: selector is 4 bytes, each param is 32 bytes
// Param 0 starts at byte 4, param 1 at byte 36, param 2 at byte 68, etc.
for (let i = 0; i < customParams.length; i++) {
if (customParams[i].enforced && customParams[i].value) {
const startIndex = 4 + (i * 32) // 4-byte selector + 32 bytes per param
const encodedValue = encodeParamValue(customParams[i].type, customParams[i].value)
// encodedValue should be a 32-byte hex string (left-padded for uint/address/bool, etc.)
caveats.push({
enforcer: addrs.allowedCalldataEnforcer,
terms: encodeAbiParameters(
[{ type: 'uint256' }, { type: 'bytes' }],
[BigInt(startIndex), encodedValue]
),
})
}
}
}
// If NO params enforced, no calldata caveat (only target + method restrictions)
// ValueLteEnforcer
caveats.push({
enforcer: addrs.valueLteEnforcer,
terms: encodePacked(['uint256'], [parseEther(maxValueEth)]),
})
// Optional: LimitedCallsEnforcer
if (maxCalls !== undefined) {
caveats.push({
enforcer: addrs.limitedCallsEnforcer,
terms: encodePacked(['uint256'], [BigInt(maxCalls)]),
})
}
// Optional: TimestampEnforcer
if (expiryTimestamp) {
const now = Math.floor(Date.now() / 1000)
caveats.push({
enforcer: addrs.timestampEnforcer,
terms: encodePacked(['uint128', 'uint128'], [BigInt(now), BigInt(expiryTimestamp)]),
})
}
return caveats
}Add helper functions:
// Encode a single parameter value as 32-byte ABI encoding
function encodeParamValue(type: string, value: string): Hex {
if (type === 'address') {
return encodeAbiParameters([{ type: 'address' }], [value as Address])
} else if (type === 'uint256') {
return encodeAbiParameters([{ type: 'uint256' }], [BigInt(value)])
} else if (type === 'bool') {
return encodeAbiParameters([{ type: 'bool' }], [value === 'true'])
} else if (type === 'bytes32') {
return encodeAbiParameters([{ type: 'bytes32' }], [value as Hex])
}
// Default: encode as bytes
return encodeAbiParameters([{ type: 'bytes' }], [value as Hex])
}
// Encode full function calldata (selector + encoded args)
function encodeFunctionCalldata(selector: Hex, params: CustomParam[]): Hex {
// Build ABI types and values from params
const types = params.map(p => ({ type: p.type }))
const values = params.map(p => {
if (p.type === 'address') return p.value as Address
if (p.type === 'uint256') return BigInt(p.value)
if (p.type === 'bool') return p.value === 'true'
if (p.type === 'bytes32') return p.value as Hex
return p.value
})
const encodedArgs = encodeAbiParameters(types, values)
return (selector + encodedArgs.slice(2)) as Hex // concat selector + encoded args
}
interface CustomParam {
type: string
value: string
name: string
locked: boolean
required: boolean
enforced: boolean // NEW: whether this param should be enforced
description: string
}State for param enforcement:
Each param row now has an "enforce" toggle. Add enforced: boolean to the customParams state:
// Existing param state becomes:
const [customParams, setCustomParams] = useState<{
type: string
value: string
name: string
locked: boolean
required: boolean
enforced: boolean // NEW
description: string
}[]>([])When applying a recipe:
- All params with
required: trueORlocked: trueshould defaultenforced: true - When manually adding params,
enforceddefaults totrue
UI for each param row:
- Add a checkbox or toggle: "🔒 Enforce" next to each param
- When checked, this param's value will be locked in the delegation
- When unchecked, the delegate can use any value for this param
- Locked params from recipes should always have enforced=true and the toggle disabled
Validation before granting:
- All
requiredparams must have values filled in - Show error: "Please fill in all required parameters" if any required param is empty
- Disable the "Grant Permission" button until all required params are filled
Visual indicators:
- Required params: show a red asterisk (*) next to the name
- Enforced params: show a lock icon 🔒
- Locked params (from recipe): grayed out input + lock icon + "Set by recipe" hint
Info text above the form: Add a brief explanation: "Enforced parameters are locked into the delegation — the delegate must use exactly these values. Unenforced parameters allow the delegate to choose any value."
Pass updated params to buildCustomActionCaveats:
const caveats = buildCustomActionCaveats(
chainId,
customTarget,
customSelector,
customParams, // Now includes enforced flag
customMaxValue,
customMaxCalls || undefined,
expiryTimestamp || undefined,
)Store which params are enforced so the redeem page knows:
// In StoredDelegation.meta for custom type
customParams?: {
name: string
type: string
value: string
enforced: boolean
locked: boolean
}[]For custom scope redemption:
- Show the enforced params and their locked values
- Show unenforced params as editable inputs
- Build execution calldata from: enforced param values (from delegation meta) + user-entered values for unenforced params
- If ALL params were enforced (ExactCalldataEnforcer), show "Execute" button only — no inputs
- If SOME params were enforced, show inputs for the unenforced ones
Example for a vote(uint256 propId, bool opinion) with propId enforced:
- Show: "propId: 42 🔒"
- Input: "opinion: [true/false dropdown]"
- User fills in opinion, clicks Execute
- Build calldata: vote(42, userChosenOpinion)
- Remove from addresses.ts if present
- Remove any imports/references
npx vite buildmust pass- No TypeScript errors
- No console errors
You are on fix/calldata-enforcers. Commit to THIS branch.