Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/7715-permission-types/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `makePermissionDecoderConfigs` to resolve the `PermissionDecoderConfig`s to be used with @metamask/gator-permissions-controller ([#259](https://github.com/MetaMask/smart-accounts-kit/pull/259))

## [0.7.1]

### Fixed
Expand Down
26 changes: 24 additions & 2 deletions packages/7715-permission-types/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,2 +1,24 @@
// eslint-disable-next-line
export { default } from '../../shared/config/base.eslint.mjs';
// eslint-disable-next-line import-x/extensions
import baseConfig from '../../shared/config/base.eslint.mjs';

const config = [
...baseConfig,
{
files: ['test/**/*.ts'],
languageOptions: {
parserOptions: {
projectService: false,
project: ['./tsconfig.eslint.json'],
},
},
settings: {
'import/resolver': {
typescript: {
project: './tsconfig.eslint.json',
},
},
},
},
];

export default config;
11 changes: 10 additions & 1 deletion packages/7715-permission-types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
"scripts": {
"build": "yarn typecheck && tsup",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"lint": "yarn lint:eslint",
"lint:eslint": "eslint . --cache --ext js,ts",
"lint:fix": "yarn lint:eslint --fix",
Expand All @@ -55,10 +57,17 @@
},
"devDependencies": {
"@metamask/auto-changelog": "^6.1.1",
"@types/node": "^20.19.0",
"compare-versions": "^6.1.1",
"eslint": "^9.39.2",
"prettier": "^3.5.3",
"tsup": "^8.5.0",
"typescript": "5.5.4"
"typescript": "5.5.4",
"vitest": "^3.2.4"
},
"dependencies": {
"@metamask/delegation-core": "^2.2.1",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move @metamask/delegation-deployments to devDependencies. I think it’s only used for tests.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

I think we could potentially remove the dependency entirely - we don't strictly need the dependency, as we could make up environment values, but it's easier to import 🤷

"@metamask/delegation-deployments": "^1.4.0",
"@metamask/utils": "^11.4.0"
}
}
4 changes: 4 additions & 0 deletions packages/7715-permission-types/src/index.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makePermissionDecoderConfigs returns PermissionDecoderConfig[] and accepts DeployedContractsByName, but neither type is exported from src/index.ts. Consumers who want to annotate a variable or write a helper that accepts/returns these types have no way to reference them. Please export both.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call! These are the types that I mentioned this morning :)

Updated!

Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ export type {
RevokeExecutionPermissionResponseResult,
MetaMaskBasePermissionData,
} from './types';

export type { PayeeRule, RedeemerRule, ExpiryRule } from './permissions';

export { makePermissionDecoderConfigs } from './permissions';
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { hexToNumber } from '@metamask/utils';

import type { Erc20TokenAllowancePermission } from '../../types';
import { expiryRuleDecoder } from '../rules/expiry';
import { erc20PayeeRuleDecoder } from '../rules/payee';
import { redeemerRuleDecoder } from '../rules/redeemer';
import type {
ChecksumCaveat,
ChecksumEnforcersByChainId,
DecodedPermissionData,
MakePermissionDecoderConfig,
} from '../types';
import {
getByteLength,
getTermsByEnforcer,
splitHex,
UINT256_MAX,
ZERO_32_BYTES,
} from '../utils';

/**
* Builds the configuration for the erc20-token-allowance permission decoder.
*
* @param contractAddresses - Checksummed enforcer addresses for the chain.
* @returns The erc20-token-allowance permission decoder configuration.
*/
export function makeErc20TokenAllowanceDecoderConfig(
contractAddresses: ChecksumEnforcersByChainId,
): MakePermissionDecoderConfig {
const {
timestampEnforcer,
erc20PeriodicEnforcer,
valueLteEnforcer,
nonceEnforcer,
allowedCalldataEnforcer,
redeemerEnforcer,
} = contractAddresses;

return {
permissionType: 'erc20-token-allowance',
contractAddresses,
optionalEnforcers: [
timestampEnforcer, // expiry rule
redeemerEnforcer, // redeemer rule
allowedCalldataEnforcer, // payee rule
],
requiredEnforcers: {
[erc20PeriodicEnforcer]: 1,
[valueLteEnforcer]: 1,
[nonceEnforcer]: 1,
},
rules: [expiryRuleDecoder, redeemerRuleDecoder, erc20PayeeRuleDecoder],
validateAndDecodeData,
};
}

/**
* Decodes erc20-token-allowance permission data from caveats; throws on invalid.
*
* @param caveats - Caveats from the permission context (checksummed).
* @param contractAddresses - Checksummed enforcer addresses for the chain.
* @returns Decoded allowance terms.
*/
function validateAndDecodeData(
caveats: ChecksumCaveat[],
contractAddresses: ChecksumEnforcersByChainId,
): DecodedPermissionData<Erc20TokenAllowancePermission> {
const { erc20PeriodicEnforcer, valueLteEnforcer } = contractAddresses;

const valueLteTerms = getTermsByEnforcer({
caveats,
enforcer: valueLteEnforcer,
});
if (valueLteTerms !== ZERO_32_BYTES) {
throw new Error(`Invalid value-lte terms: must be ${ZERO_32_BYTES}`);
}

const terms = getTermsByEnforcer({
caveats,
enforcer: erc20PeriodicEnforcer,
});

const EXPECTED_TERMS_BYTELENGTH = 116; // 20 + 32 + 32 + 32

if (getByteLength(terms) !== EXPECTED_TERMS_BYTELENGTH) {
throw new Error('Invalid erc20-token-allowance terms: expected 116 bytes');
}

const [tokenAddress, allowanceAmount, periodDurationRaw, startTimeRaw] =
splitHex(terms, [20, 32, 32, 32]);
if (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

splitHex always prepends 0x to every segment, so every returned value is truthy — the if (!tokenAddress || !allowanceAmount || ...) guard can never fire. The byte-length check above it is the real defence. Please remove this dead guard

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above - fixed with an update to splitHex 👍

!tokenAddress ||
!allowanceAmount ||
!periodDurationRaw ||
!startTimeRaw
) {
throw new Error('Invalid erc20-token-allowance terms');
}

if (periodDurationRaw.toLowerCase() !== UINT256_MAX) {
throw new Error(
'Invalid erc20-token-allowance terms: periodDuration must be UINT256_MAX',
);
}

const startTime = hexToNumber(startTimeRaw);

if (startTime === 0) {
throw new Error(
'Invalid erc20-token-allowance terms: startTime must be a positive number',
);
}

if (allowanceAmount === ZERO_32_BYTES) {
throw new Error(
'Invalid erc20-token-allowance terms: allowanceAmount must be a positive number',
);
}

return { tokenAddress, allowanceAmount, startTime };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { decodeERC20TokenPeriodTransferTerms } from '@metamask/delegation-core';
import { bigIntToHex } from '@metamask/utils';

import type { Erc20TokenPeriodicPermission } from '../../types';
import { expiryRuleDecoder } from '../rules/expiry';
import { erc20PayeeRuleDecoder } from '../rules/payee';
import { redeemerRuleDecoder } from '../rules/redeemer';
import type {
ChecksumCaveat,
ChecksumEnforcersByChainId,
DecodedPermissionData,
MakePermissionDecoderConfig,
} from '../types';
import {
getTermsByEnforcer,
MAX_PERIOD_DURATION,
ZERO_32_BYTES,
} from '../utils';

/**
* Builds the configuration for the erc20-token-periodic permission decoder.
*
* @param contractAddresses - Checksummed enforcer addresses for the chain.
* @returns The erc20-token-periodic permission decoder configuration.
*/
export function makeErc20TokenPeriodicDecoderConfig(
contractAddresses: ChecksumEnforcersByChainId,
): MakePermissionDecoderConfig {
const {
timestampEnforcer,
erc20PeriodicEnforcer,
valueLteEnforcer,
nonceEnforcer,
allowedCalldataEnforcer,
redeemerEnforcer,
} = contractAddresses;

return {
permissionType: 'erc20-token-periodic',
contractAddresses,
optionalEnforcers: [
timestampEnforcer, // expiry rule
redeemerEnforcer, // redeemer rule
allowedCalldataEnforcer, // payee rule
],
requiredEnforcers: {
[erc20PeriodicEnforcer]: 1,
[valueLteEnforcer]: 1,
[nonceEnforcer]: 1,
},
rules: [expiryRuleDecoder, redeemerRuleDecoder, erc20PayeeRuleDecoder],
validateAndDecodeData,
};
}

/**
* Decodes erc20-token-periodic permission data from caveats; throws on invalid.
*
* @param caveats - Caveats from the permission context (checksummed).
* @param contractAddresses - Checksummed enforcer addresses for the chain.
* @returns Decoded periodic terms.
*/
function validateAndDecodeData(
caveats: ChecksumCaveat[],
contractAddresses: ChecksumEnforcersByChainId,
): DecodedPermissionData<Erc20TokenPeriodicPermission> {
const { erc20PeriodicEnforcer, valueLteEnforcer } = contractAddresses;

const valueLteTerms = getTermsByEnforcer({
caveats,
enforcer: valueLteEnforcer,
});
if (valueLteTerms !== ZERO_32_BYTES) {
throw new Error(`Invalid value-lte terms: must be ${ZERO_32_BYTES}`);
}

const terms = getTermsByEnforcer({
caveats,
enforcer: erc20PeriodicEnforcer,
});

const {
tokenAddress,
periodAmount,
periodDuration,
startDate: startTime,
} = decodeERC20TokenPeriodTransferTerms(terms);

if (periodAmount === 0n) {
throw new Error(
'Invalid erc20-token-periodic terms: periodAmount must be a positive number',
);
}

if (periodDuration === 0) {
throw new Error(
'Invalid erc20-token-periodic terms: periodDuration must be a positive number',
);
}

if (periodDuration > MAX_PERIOD_DURATION) {
throw new Error(
'Invalid erc20-token-periodic terms: periodDuration must be less than or equal to MAX_PERIOD_DURATION',
);
}

if (startTime === 0) {
throw new Error(
'Invalid erc20-token-periodic terms: startTime must be a positive number',
);
}

return {
tokenAddress,
periodAmount: bigIntToHex(periodAmount),
periodDuration,
startTime,
};
}
Loading
Loading