From 8adad51093239f4852d1bb2ed3ba0489bf8a978c Mon Sep 17 00:00:00 2001 From: Maxime Date: Thu, 22 Feb 2024 13:20:43 +0000 Subject: [PATCH] feat: add ALL_PERMISSIONS flag to encoding --- package-lock.json | 4 +- src/constants/constants.ts | 3 +- src/index.test.ts | 116 +++++++++++++++++++++++++++++++++++++ src/index.ts | 53 +++++++++++++++-- src/types/Method.ts | 1 + test/testHelpers.ts | 18 ++++++ 6 files changed, 186 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae367a79..d734fda1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@erc725/erc725.js", - "version": "0.17.2", + "version": "0.21.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@erc725/erc725.js", - "version": "0.17.2", + "version": "0.21.3", "license": "Apache-2.0", "dependencies": { "add": "^2.0.6", diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 85e19aea..f28a6852 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -179,7 +179,8 @@ export const LSP6_DEFAULT_PERMISSIONS = { DECRYPT : "0x0000000000000000000000000000000000000000000000000000000000100000", SIGN : "0x0000000000000000000000000000000000000000000000000000000000200000", EXECUTE_RELAY_CALL : "0x0000000000000000000000000000000000000000000000000000000000400000", - ERC4337_PERMISSION : "0x0000000000000000000000000000000000000000000000000000000000800000" + ERC4337_PERMISSION : "0x0000000000000000000000000000000000000000000000000000000000800000", + ALL_PERMISSIONS : "0x00000000000000000000000000000000000000000000000000000000007f3f7f" // lsp6 v0.14.0 }; export const LSP6_ALL_PERMISSIONS = diff --git a/src/index.test.ts b/src/index.test.ts index c498e50f..561480b2 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -47,6 +47,7 @@ import 'isomorphic-fetch'; import { ERC725Y_INTERFACE_IDS, + LSP6_DEFAULT_PERMISSIONS, SUPPORTED_VERIFICATION_METHOD_STRINGS, } from './constants/constants'; import { decodeKey } from './lib/decodeData'; @@ -1192,6 +1193,7 @@ describe('Running @erc725/erc725.js tests...', () => { SIGN: true, EXECUTE_RELAY_CALL: false, ERC4337_PERMISSION: false, + ALL_PERMISSIONS: false, }, hex: '0x00000000000000000000000000000000000000000000000000000000003f3f7f', }, @@ -1221,6 +1223,7 @@ describe('Running @erc725/erc725.js tests...', () => { SIGN: false, EXECUTE_RELAY_CALL: false, ERC4337_PERMISSION: false, + ALL_PERMISSIONS: false, }, hex: '0x0000000000000000000000000000000000000000000000000000000000000000', }, @@ -1250,6 +1253,7 @@ describe('Running @erc725/erc725.js tests...', () => { SIGN: true, EXECUTE_RELAY_CALL: false, ERC4337_PERMISSION: false, + ALL_PERMISSIONS: false, }, hex: '0x0000000000000000000000000000000000000000000000000000000000200a00', }, @@ -1279,6 +1283,7 @@ describe('Running @erc725/erc725.js tests...', () => { SIGN: false, EXECUTE_RELAY_CALL: false, ERC4337_PERMISSION: false, + ALL_PERMISSIONS: false, }, hex: '0x0000000000000000000000000000000000000000000000000000000000040800', }, @@ -1308,6 +1313,7 @@ describe('Running @erc725/erc725.js tests...', () => { SIGN: false, EXECUTE_RELAY_CALL: false, ERC4337_PERMISSION: false, + ALL_PERMISSIONS: false, }, hex: '0x0000000000000000000000000000000000000000000000000000000000040004', }, @@ -1337,6 +1343,7 @@ describe('Running @erc725/erc725.js tests...', () => { SIGN: false, EXECUTE_RELAY_CALL: false, ERC4337_PERMISSION: false, + ALL_PERMISSIONS: false, }, hex: '0x0000000000000000000000000000000000000000000000000000000000000a00', }, @@ -1376,6 +1383,112 @@ describe('Running @erc725/erc725.js tests...', () => { }); }); + describe('Randomized Permissions Encoding', () => { + function convertToPermissionBits(permissions: { [key: string]: string }) { + const permissionBits = {}; + Object.entries(permissions).forEach(([key, hexValue]) => { + // Convert hex to binary, then find the position of the '1' bit + const bitPosition = BigInt(hexValue).toString(2).length - 1; + permissionBits[key] = bitPosition; + }); + return permissionBits; + } + + // remove LSP6_DEFAULT_PERMISSIONS.ALL_PERMISSIONS from LSP6_DEFAULT_PERMISSIONS + const ALL_PERMISSIONS_WITHOUT_ALL_PERMISSIONS = Object.keys( + LSP6_DEFAULT_PERMISSIONS, + ).reduce((acc, key) => { + if (key !== 'ALL_PERMISSIONS') { + acc[key] = LSP6_DEFAULT_PERMISSIONS[key]; + } + return acc; + }, {}); + + // Use the function to generate permissionBits + const permissionBits = convertToPermissionBits( + ALL_PERMISSIONS_WITHOUT_ALL_PERMISSIONS, + ); + + // Function to generate a random permissions object + const generateRandomPermissions = () => { + return Object.keys(permissionBits).reduce((acc, key) => { + // Randomly assign true or false + acc[key] = Math.random() < 0.5; + return acc; + }, {}); + }; + + // Function to calculate expected hex based on permissions + const calculateExpectedHex = (permissions) => { + let basePermissions = BigInt(0); + Object.entries(permissions).forEach(([key, value]) => { + if (value) { + const bitPosition = permissionBits[key]; + basePermissions |= BigInt(1) << BigInt(bitPosition); + } + }); + // Convert to hex string, properly padded + return `0x${basePermissions.toString(16).padStart(64, '0')}`; + }; + + // Run the randomized test multiple times + const numberOfTests = 100; // Number of random tests + for (let i = 0; i < numberOfTests; i++) { + it(`Randomized test #${i + 1}`, () => { + const randomPermissions = generateRandomPermissions(); + const encoded = ERC725.encodePermissions(randomPermissions); + const expectedHex = calculateExpectedHex(randomPermissions); + assert.strictEqual( + encoded, + expectedHex, + `Failed at randomized test #${i + 1}`, + ); + }); + } + }); + + describe('all permissions', () => { + it('should encode ALL_PERMISSIONS correctly', () => { + const permissions = { ALL_PERMISSIONS: true }; + const encoded = ERC725.encodePermissions(permissions); + + assert.strictEqual( + encoded, + LSP6_DEFAULT_PERMISSIONS.ALL_PERMISSIONS, + 'Encoded permissions do not match expected value for ALL_PERMISSIONS', + ); + }); + + it('should ignore individual permissions when ALL_PERMISSIONS is set', () => { + const permissions = { + ALL_PERMISSIONS: true, + CHANGEOWNER: true, + }; + const encoded = ERC725.encodePermissions(permissions); + assert.strictEqual( + encoded, + LSP6_DEFAULT_PERMISSIONS.ALL_PERMISSIONS, + 'ALL_PERMISSIONS did not correctly encode with additional permissions', + ); + }); + it('should be able to disable permissions that are part of ALL_PERMISSIONS', () => { + const permissions = { + ALL_PERMISSIONS: true, + CHANGEOWNER: false, // Explicitly disable CHANGEOWNER + }; + + const encoded = ERC725.encodePermissions(permissions); + const decodedPermissions = ERC725.decodePermissions(encoded); + + // check that the permission is disabled + assert.strictEqual( + decodedPermissions.CHANGEOWNER, + false, + 'CHANGEOWNER permission was not correctly disabled', + ); + }); + }); + describe('decodePermissions', () => { testCases.forEach((testCase) => { it(`Decodes ${testCase.hex} permission correctly`, () => { @@ -1383,6 +1496,7 @@ describe('Running @erc725/erc725.js tests...', () => { ERC725.decodePermissions(testCase.hex), testCase.permissions, ); + assert.deepStrictEqual( erc725Instance.decodePermissions(testCase.hex), testCase.permissions, @@ -1419,6 +1533,7 @@ describe('Running @erc725/erc725.js tests...', () => { SIGN: true, EXECUTE_RELAY_CALL: true, ERC4337_PERMISSION: true, + ALL_PERMISSIONS: true, }, ); assert.deepStrictEqual( @@ -1450,6 +1565,7 @@ describe('Running @erc725/erc725.js tests...', () => { SIGN: true, EXECUTE_RELAY_CALL: true, ERC4337_PERMISSION: true, + ALL_PERMISSIONS: true, }, ); }); diff --git a/src/index.ts b/src/index.ts index d797aa50..0f38da8e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -450,13 +450,53 @@ export class ERC725 { * @returns {*} The permissions encoded as a hexadecimal string as defined by the LSP6 Standard. */ static encodePermissions(permissions: Permissions): string { - const result = Object.keys(permissions).reduce((previous, key) => { - return permissions[key] - ? previous | Number(hexToNumber(LSP6_DEFAULT_PERMISSIONS[key])) - : previous; - }, 0); + let basePermissions = BigInt(0); - return leftPad(toHex(result), 64); + // If ALL_PERMISSIONS is requested, start with that as the base + if (permissions.ALL_PERMISSIONS) { + basePermissions = BigInt( + hexToNumber(LSP6_DEFAULT_PERMISSIONS.ALL_PERMISSIONS), + ); + } + + // Explicitly add REENTRANCY, DELEGATECALL, and SUPER_DELEGATECALL if requested (they are not included in ALL_PERMISSIONS) + const additionalPermissions = [ + LSP6_DEFAULT_PERMISSIONS.REENTRANCY, + LSP6_DEFAULT_PERMISSIONS.DELEGATECALL, + LSP6_DEFAULT_PERMISSIONS.SUPER_DELEGATECALL, + ]; + additionalPermissions.forEach((permission) => { + if (permissions[permission]) { + basePermissions |= BigInt( + hexToNumber(LSP6_DEFAULT_PERMISSIONS[permission]), + ); + } + }); + + // Process each permission to potentially switch off permissions included in ALL_PERMISSIONS + Object.keys(permissions).forEach((key) => { + const permissionValue = BigInt( + hexToNumber(LSP6_DEFAULT_PERMISSIONS[key]), + ); + + if (permissions[key]) { + // If not dealing with ALL_PERMISSIONS or additional permissions, ensure they are added + if ( + !additionalPermissions.includes(key) && + key !== LSP6_DEFAULT_PERMISSIONS.ALL_PERMISSIONS + ) { + basePermissions |= permissionValue; + } + } else if ( + LSP6_DEFAULT_PERMISSIONS[key] !== + LSP6_DEFAULT_PERMISSIONS.ALL_PERMISSIONS + ) { + // If permission is set to false, remove it from the basePermissions + basePermissions &= ~permissionValue; + } + }); + // Convert the final BigInt permission value back to a hex string, properly padded + return leftPad(toHex(basePermissions.toString()), 64); } /** @@ -503,6 +543,7 @@ export class ERC725 { SIGN: false, EXECUTE_RELAY_CALL: false, ERC4337_PERMISSION: false, + ALL_PERMISSIONS: false, }; const permissionsToTest = Object.keys(LSP6_DEFAULT_PERMISSIONS); diff --git a/src/types/Method.ts b/src/types/Method.ts index 681aee85..6eb49bb9 100644 --- a/src/types/Method.ts +++ b/src/types/Method.ts @@ -48,4 +48,5 @@ export interface Permissions { SIGN?: boolean; EXECUTE_RELAY_CALL?: boolean; ERC4337_PERMISSION?: boolean; + ALL_PERMISSIONS?: boolean; } diff --git a/test/testHelpers.ts b/test/testHelpers.ts index c51c3cdd..b1547472 100644 --- a/test/testHelpers.ts +++ b/test/testHelpers.ts @@ -111,3 +111,21 @@ export function generateAllResults(schemas) { }; }); } + +export function hexToBitPositions(hexString) { + const binaryString = BigInt(hexString).toString(2); + + const bitPositions: number[] = []; + + for (let i = binaryString.length - 1; i >= 0; i--) { + // The current bit is set to 1 + if (binaryString[i] === '1') { + // Calculate the bit position. We subtract from the string's length to start counting from 0 + const bitPosition = binaryString.length - 1 - i; + bitPositions.push(bitPosition); + } + } + + // Return the array of bit positions + return bitPositions; +}