Skip to content

Commit

Permalink
feat: add ALL_PERMISSIONS flag to encoding
Browse files Browse the repository at this point in the history
  • Loading branch information
skimaharvey committed Feb 22, 2024
1 parent e46dfd8 commit 8adad51
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 9 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
116 changes: 116 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -1192,6 +1193,7 @@ describe('Running @erc725/erc725.js tests...', () => {
SIGN: true,
EXECUTE_RELAY_CALL: false,
ERC4337_PERMISSION: false,
ALL_PERMISSIONS: false,
},
hex: '0x00000000000000000000000000000000000000000000000000000000003f3f7f',
},
Expand Down Expand Up @@ -1221,6 +1223,7 @@ describe('Running @erc725/erc725.js tests...', () => {
SIGN: false,
EXECUTE_RELAY_CALL: false,
ERC4337_PERMISSION: false,
ALL_PERMISSIONS: false,
},
hex: '0x0000000000000000000000000000000000000000000000000000000000000000',
},
Expand Down Expand Up @@ -1250,6 +1253,7 @@ describe('Running @erc725/erc725.js tests...', () => {
SIGN: true,
EXECUTE_RELAY_CALL: false,
ERC4337_PERMISSION: false,
ALL_PERMISSIONS: false,
},
hex: '0x0000000000000000000000000000000000000000000000000000000000200a00',
},
Expand Down Expand Up @@ -1279,6 +1283,7 @@ describe('Running @erc725/erc725.js tests...', () => {
SIGN: false,
EXECUTE_RELAY_CALL: false,
ERC4337_PERMISSION: false,
ALL_PERMISSIONS: false,
},
hex: '0x0000000000000000000000000000000000000000000000000000000000040800',
},
Expand Down Expand Up @@ -1308,6 +1313,7 @@ describe('Running @erc725/erc725.js tests...', () => {
SIGN: false,
EXECUTE_RELAY_CALL: false,
ERC4337_PERMISSION: false,
ALL_PERMISSIONS: false,
},
hex: '0x0000000000000000000000000000000000000000000000000000000000040004',
},
Expand Down Expand Up @@ -1337,6 +1343,7 @@ describe('Running @erc725/erc725.js tests...', () => {
SIGN: false,
EXECUTE_RELAY_CALL: false,
ERC4337_PERMISSION: false,
ALL_PERMISSIONS: false,
},
hex: '0x0000000000000000000000000000000000000000000000000000000000000a00',
},
Expand Down Expand Up @@ -1376,13 +1383,120 @@ 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`, () => {
assert.deepStrictEqual(
ERC725.decodePermissions(testCase.hex),
testCase.permissions,
);

assert.deepStrictEqual(
erc725Instance.decodePermissions(testCase.hex),
testCase.permissions,
Expand Down Expand Up @@ -1419,6 +1533,7 @@ describe('Running @erc725/erc725.js tests...', () => {
SIGN: true,
EXECUTE_RELAY_CALL: true,
ERC4337_PERMISSION: true,
ALL_PERMISSIONS: true,
},
);
assert.deepStrictEqual(
Expand Down Expand Up @@ -1450,6 +1565,7 @@ describe('Running @erc725/erc725.js tests...', () => {
SIGN: true,
EXECUTE_RELAY_CALL: true,
ERC4337_PERMISSION: true,
ALL_PERMISSIONS: true,
},
);
});
Expand Down
53 changes: 47 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/types/Method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ export interface Permissions {
SIGN?: boolean;
EXECUTE_RELAY_CALL?: boolean;
ERC4337_PERMISSION?: boolean;
ALL_PERMISSIONS?: boolean;
}
18 changes: 18 additions & 0 deletions test/testHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

0 comments on commit 8adad51

Please sign in to comment.