Skip to content

Commit

Permalink
streamline l2: get rid of special inputsMatch entrypoint
Browse files Browse the repository at this point in the history
  • Loading branch information
jfschwarz committed Apr 20, 2023
1 parent 8ef645e commit 9b2cc18
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 102 deletions.
17 changes: 9 additions & 8 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,13 @@ It also offers various sanity checks and normalizations for conditions.

A permission preset is a parametrized set of permissions. It can be applied to different roles, filling in the specific parameter values for the placeholders in conditions.

On this layer, the sdk offers the `inputsMatch()` helper function for defining conditions, relying on user-provided ABIs for contract function parameters.

**Example:**

```javascript
{
targetAddress: '0x182B723a58739a9c974cFDB385ceaDb237453c28',
signature: "claim_rewards(address)",
condition: inputsMatch([AVATAR], ["address"]),
condition: c.matchesAbi([AVATAR], ["address"]),
}
```

Expand Down Expand Up @@ -87,7 +85,7 @@ Matching patterns can also be supplied as arrays using the `matches([ 1, undefin
- `eq`
- `lt`
- `gt`
- `bitmask`
- `bitmask` _not yet implemented_

#### Array conditions

Expand All @@ -99,7 +97,6 @@ Matching patterns can also be supplied as arrays using the `matches([ 1, undefin

- `and`
- `or`
- `xor`
- `nor`

#### Tuple matching
Expand All @@ -109,6 +106,10 @@ Matching patterns can also be supplied as arrays using the `matches([ 1, undefin

#### Allowance conditions

- `withinAllowance`
- `etherWithinAllowance`
- `callWithinAllowance`
- `withinAllowance` _not yet implemented_
- `etherWithinAllowance` _not yet implemented_
- `callWithinAllowance` _not yet implemented_

#### Custom conditions

- `custom` _not yet implemented_
6 changes: 5 additions & 1 deletion packages/sdk/src/presets/authoring/batching.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { BytesLike } from "ethers"

import { ExecutionFlags, PresetAllowEntry, PresetCondition } from "../types"

import { ConditionFunction } from "./conditions/types"

type PartialPresetFullyClearedTarget = ExecutionFlags
type PartialPresetFunction = ({ sighash: string } | { signature: string }) & {
condition?: PresetCondition
condition?: PresetCondition | ConditionFunction<BytesLike>
} & ExecutionFlags

export const forAll = (
Expand Down
4 changes: 2 additions & 2 deletions packages/sdk/src/presets/authoring/conditions/matches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ export const matchesAbi =
const paramTypes = abiTypes.map((abiType) => ParamType.from(abiType))

// only supported at the top level or for bytes type params
if (abiType && abiType.name !== "bytes") {
if (abiType && abiType.type !== "bytes") {
throw new Error(
`Can only use \`matchesAbi\` on bytes types params, got: ${abiType.type}`
`Can only use \`matchesAbi\` on bytes type params, got: ${abiType.type}`
)
}

Expand Down
8 changes: 4 additions & 4 deletions packages/sdk/src/presets/authoring/erc20.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Placeholder } from "../types"

import { forAll } from "./batching"
import { matchesAbi } from "./conditions"
import { or } from "./conditions/branching"
import { inputsMatch } from "./inputsMatch"

export const allowErc20Approve = (
tokens: string[],
Expand All @@ -12,7 +12,7 @@ export const allowErc20Approve = (

return forAll(tokens, {
signature: "approve(address,uint256)",
condition: inputsMatch(
condition: matchesAbi(
[
spenders.length === 1
? spenders[0]
Expand All @@ -33,7 +33,7 @@ export const allowErc20Revoke = (
signature: "approve(address,uint256)",
condition:
spenders &&
inputsMatch(
matchesAbi(
[
spenders.length === 1
? spenders[0]
Expand All @@ -50,7 +50,7 @@ export const allowErc20Transfer = (tokens: string[], recipients: string[]) => {

return forAll(tokens, {
signature: "transfer(address,uint256)",
condition: inputsMatch(
condition: matchesAbi(
[
recipients.length === 1
? recipients[0]
Expand Down
1 change: 0 additions & 1 deletion packages/sdk/src/presets/authoring/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { inputsMatch } from "./inputsMatch"
export { allow, EVERYTHING, ethSdk } from "./kit"
21 changes: 0 additions & 21 deletions packages/sdk/src/presets/authoring/inputsMatch.ts

This file was deleted.

31 changes: 15 additions & 16 deletions packages/sdk/src/presets/fillPreset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import {
Placeholder,
ComparisonValue,
PresetCondition,
PresetFunctionCoerced,
} from "./types"
import { functionId, isScoped, sighash } from "./utils"
import { allowEntryId, isScoped } from "./utils"

/**
* Processes a RolePreset, filling in the placeholders and returning the final permissions
Expand All @@ -32,15 +33,17 @@ export const fillPreset = <P extends PermissionPreset>(
preset: P,
placeholderValues: PlaceholderValues<P>
): Target[] => {
preset = mergeFunctionEntries(preset) as P
sanityCheck(preset)
const mergedPreset = mergeFunctionEntries(preset)
sanityCheck(mergedPreset)

const placeholderLookupMap = makePlaceholderLookupMap(
preset,
mergedPreset,
placeholderValues
)

const fullyClearedTargets = preset.allow
const { allow } = mergedPreset

const fullyClearedTargets = allow
.filter((entry) => !isScoped(entry))
.map((entry) => ({
address: entry.targetAddress.toLowerCase(),
Expand All @@ -49,24 +52,20 @@ export const fillPreset = <P extends PermissionPreset>(
functions: [],
}))

const allowFunctionEntries = allow.filter(
(entry) => "selector" in entry
) as PresetFunctionCoerced[]

const functionScopedTargets = Object.entries(
groupBy(preset.allow.filter(isScoped), (entry) =>
entry.targetAddress.toLowerCase()
)
groupBy(allowFunctionEntries, (entry) => entry.targetAddress)
).map(([targetAddress, allowFunctions]) => ({
address: targetAddress.toLowerCase(),
clearance: Clearance.Function,
executionOptions: ExecutionOptions.None,
functions: allowFunctions.map((allowFunction) => {
let selector = "selector" in allowFunction && allowFunction.selector
if (!selector) {
selector =
"signature" in allowFunction && sighash(allowFunction.signature)
}
if (!selector) throw new Error("invariant violation")
const { condition } = allowFunction
return {
selector,
selector: allowFunction.selector,
executionOptions: execOptions(allowFunction),
wildcarded: !condition,
condition:
Expand Down Expand Up @@ -168,7 +167,7 @@ const assertNoWildcardScopedIntersection = (preset: PermissionPreset) => {
}

const assertNoDuplicateAllowFunction = (preset: PermissionPreset) => {
const allowFunctions = preset.allow.filter(isScoped).map(functionId)
const allowFunctions = preset.allow.filter(isScoped).map(allowEntryId)

const counts = allowFunctions.reduce(
(result, item) => ({ ...result, [item]: (result[item] || 0) + 1 }),
Expand Down
42 changes: 23 additions & 19 deletions packages/sdk/src/presets/mergeFunctionEntries.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
import { Operator, ParameterType } from "../types"

import {
PresetAllowEntry,
PermissionPreset,
PresetFunction,
PresetCondition,
PresetAllowEntryCoerced,
PresetFunctionCoerced,
} from "./types"
import { functionId, isScoped } from "./utils"
import { coercePresetFunction, allowEntryId, isScoped } from "./utils"

/**
* Processes the allow entries of a preset and merges entries addressing the same function into a single entry.
* This is done by merging the conditions using a logical OR.
* @param preset The preset to process
* @returns The updated preset
*/
export const mergeFunctionEntries = (
preset: PermissionPreset
): PermissionPreset => ({
export const mergeFunctionEntries = (preset: PermissionPreset) => ({
...preset,
allow: preset.allow.reduce((result, entry) => {
if (!isScoped(entry)) {
result.push(entry)
result.push({
...entry,
targetAddress: entry.targetAddress.toLowerCase(),
})
return result
}

const matchingEntry = result
.filter(isScoped)
.find((existingEntry) => functionId(existingEntry) === functionId(entry))
const coercedEntry = coercePresetFunction(entry)

const matchingEntry = result.find(
(existingEntry) =>
allowEntryId(existingEntry) === allowEntryId(coercedEntry)
) as PresetFunctionCoerced | undefined

if (!matchingEntry) {
result.push(entry)
result.push(coercedEntry)
return result
}

Expand All @@ -37,29 +42,28 @@ export const mergeFunctionEntries = (
!!matchingEntry.delegatecall !== !!entry.delegatecall
) {
// we don't merge if execution options are different
result.push(entry)
result.push(coercedEntry)
return result
}

// merge conditions into the entry we already have
matchingEntry.condition = mergeConditions(matchingEntry, entry)
matchingEntry.condition = mergeConditions(matchingEntry, coercedEntry)
return result
}, [] as PresetAllowEntry[]),
}, [] as PresetAllowEntryCoerced[]),
})

/**
* @dev Merges two conditions using a logical OR, flattening nested OR conditions. If the conditions are equal, it will still create separate OR branches.
* These will be pruned later in sanitizeCondition().
*/
const mergeConditions = (
a: PresetFunction,
b: PresetFunction
a: PresetFunctionCoerced,
b: PresetFunctionCoerced
): PresetCondition | undefined => {
if (!!a.condition !== !!b.condition) {
const targetId = allowEntryId(a)
console.warn(
`Target ${functionId(
a
)} is allowed with and without conditions. It will be allowed without conditions.`
`Target ${targetId} is allowed with and without conditions. It will be allowed without conditions.`
)
return undefined
}
Expand Down
13 changes: 12 additions & 1 deletion packages/sdk/src/presets/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ParamType } from "ethers/lib/utils"
import { BytesLike, ParamType } from "ethers/lib/utils"

import { Operator, ParameterType } from "../types"

import { ConditionFunction } from "./authoring/conditions/types"

export type AbiType = string | ParamType

export interface ExecutionFlags {
Expand Down Expand Up @@ -60,11 +62,20 @@ export type PresetFullyClearedTarget = {

// allows calls to specific functions, optionally with parameter scoping
export type PresetFunction = ({ selector: string } | { signature: string }) & {
targetAddress: string
condition?: PresetCondition | ConditionFunction<BytesLike> // condition entrypoint can be a condition function that will be invoked with `bytes` abiType (undecoded calldata)
} & ExecutionFlags

export type PresetFunctionCoerced = {
selector: string
targetAddress: string
condition?: PresetCondition
} & ExecutionFlags

export type PresetAllowEntry = PresetFullyClearedTarget | PresetFunction
export type PresetAllowEntryCoerced =
| PresetFullyClearedTarget
| PresetFunctionCoerced

export type ComparisonValue = string | Placeholder<any>

Expand Down
42 changes: 33 additions & 9 deletions packages/sdk/src/presets/utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,38 @@
import { keccak256, toUtf8Bytes } from "ethers/lib/utils"
import { keccak256, ParamType, toUtf8Bytes } from "ethers/lib/utils"

import { PresetAllowEntry, PresetFunction } from "./types"
import {
PresetAllowEntry,
PresetAllowEntryCoerced,
PresetFunction,
PresetFunctionCoerced,
} from "./types"

export const sighash = (signature: string): string =>
const sighash = (signature: string): string =>
keccak256(toUtf8Bytes(signature)).substring(0, 10)

export const isScoped = (entry: PresetAllowEntry): entry is PresetFunction =>
"selector" in entry || "signature" in entry
export const coercePresetFunction = (
entry: PresetFunction
): PresetFunctionCoerced => {
return {
targetAddress: entry.targetAddress.toLowerCase(),
selector:
"selector" in entry
? entry.selector.toLowerCase()
: sighash(entry.signature),
condition:
typeof entry.condition === "function"
? entry.condition(ParamType.from("bytes"))
: entry.condition,
send: entry.send,
delegatecall: entry.delegatecall,
}
}

export const functionId = (entry: PresetFunction) =>
`${entry.targetAddress.toLowerCase()}.${
"selector" in entry ? entry.selector : sighash(entry.signature)
}`
export const isScoped = (entry: PresetAllowEntry): entry is PresetFunction => {
return "selector" in entry || "signature" in entry
}

export const allowEntryId = (entry: PresetAllowEntryCoerced) =>
"selector" in entry
? `${entry.targetAddress.toLowerCase()}.${entry.selector}`
: entry.targetAddress.toLowerCase()
Loading

0 comments on commit 9b2cc18

Please sign in to comment.