Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Diamond Helper Functions #158

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a7edda4
diamond utils draft
0xCourtney Nov 10, 2022
420964c
Merge branch 'master' into diamond-helper
ItsNickBarry Dec 14, 2022
047964e
remove printDiamond
0xCourtney Dec 11, 2022
9d3a62d
removes composite type
0xCourtney Dec 11, 2022
9d3b060
exports facet cut action
0xCourtney Dec 11, 2022
20bf78e
removes cli-table3 package
0xCourtney Dec 11, 2022
b2eda57
imports ethersproject packages
0xCourtney Dec 11, 2022
398ff37
Revert "remove printDiamond"
0xCourtney Dec 14, 2022
f9a6c2c
removes printDiamond from external interface
0xCourtney Dec 14, 2022
45b2dc4
throws if add/replace/remove fails
0xCourtney Dec 14, 2022
3d9d456
adds only filter, refactors exclude
0xCourtney Dec 19, 2022
e702c36
adds previewFacetCut
0xCourtney Dec 19, 2022
5f34d05
removes printDiamond
0xCourtney Dec 19, 2022
1aa7db8
adds comments
0xCourtney Dec 20, 2022
f00863e
capitalize FacetCutAction enum types
ItsNickBarry Dec 20, 2022
c8195bc
moves diamond utils to diamond subdirectory
0xCourtney Dec 20, 2022
f43c14a
Merge branch 'diamond-helper' of github.com:solidstate-network/solids…
ItsNickBarry Dec 20, 2022
c93c0a5
Merge branch 'master' into diamond-helper
ItsNickBarry Dec 20, 2022
e5d19a2
use FacetCutAction enum in spec and test files
ItsNickBarry Dec 20, 2022
dfac641
fix dependency versions
ItsNickBarry Dec 20, 2022
669a620
Merge pull request #176 from solidstate-network/diamond-helper-enum-e…
ItsNickBarry Dec 20, 2022
cef5a3f
reorder declarations
ItsNickBarry Dec 20, 2022
ce286c6
uses except instead of exclude
0xCourtney Dec 20, 2022
a2a3c19
Update lib/diamond/utils.ts
0xCourtney Dec 22, 2022
c7df310
Update lib/diamond/utils.ts
0xCourtney Dec 22, 2022
7d6598e
Update lib/diamond/filters.ts
0xCourtney Dec 22, 2022
9d92ea0
rename
0xCourtney Dec 22, 2022
a9b12a5
updates comments
0xCourtney Dec 22, 2022
f4c211b
rename
0xCourtney Dec 22, 2022
dbf11d5
typo
0xCourtney Dec 22, 2022
871f868
diamonCut returns receipt
0xCourtney Dec 22, 2022
54da442
consolidates only and execpt to single array, adds type and action to…
0xCourtney Dec 22, 2022
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
57 changes: 57 additions & 0 deletions lib/diamond/filters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { AddressZero } from '@ethersproject/constants';

export interface FacetFilter {
contract: string;
selectors: string[];
}
Copy link
Member

Choose a reason for hiding this comment

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

It looks like these FacetFilter objects are used in sets of 3 arrays, corresponding to the FacetCutAction types. Would it not be better to include the action in the FacetFilter type?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can certainly look into this, I was already planning to replace the only and except parameters with filter in the add/replace/remove API, then add a type field to FacetFilter. The new FacetFilter could look something like this:

export interface FacetFilter {
  type: string; // "only" or "except"
  action: FacetCutAction;
  contract: string;
  selectors: string[];
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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


// returns true if the selector is found in the only or except filters
export function selectorIsFiltered(
only: FacetFilter[],
except: FacetFilter[],
contract: string,
selector: string,
): boolean {
if (only.length > 0) {
// include selectors found in only, exclude all others
return includes(only, contract, selector);
}

if (except.length > 0) {
// exclude selectors found in except, include all others
return !includes(except, contract, selector);
}

// if neither only or except are used, then include all selectors
return true;
}

// returns true if the selector is found in the filters
export function includes(
filters: FacetFilter[],
contract: string,
selector: string,
): boolean {
for (const filter of filters) {
if (filter.contract === contract || AddressZero === contract) {
return filter.selectors.includes(selector);
}
}

return false;
0xCourtney marked this conversation as resolved.
Show resolved Hide resolved
}

// validates that only and except filters do not contain the same contract
export function validateFilters(only: FacetFilter[], except: FacetFilter[]) {
if (only.length > 0 && except.length > 0) {
for (const onlyFilter of only) {
for (const exceptFilter of except) {
if (onlyFilter.contract === exceptFilter.contract) {
throw new Error(
'only and except filters cannot contain the same contract',
);
}
}
}
}
}
305 changes: 305 additions & 0 deletions lib/diamond/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
import { FacetFilter, selectorIsFiltered, validateFilters } from './filters';
import { AddressZero } from '@ethersproject/constants';
import { Contract } from '@ethersproject/contracts';
import {
IDiamondReadable,
IDiamondWritable,
} from '@solidstate/typechain-types';

export enum FacetCutAction {
ADD,
REPLACE,
REMOVE,
}

export interface Facet {
target: string;
selectors: string[];
}

export interface FacetCut extends Facet {
action: FacetCutAction;
}

// returns a list of signatures for a contract
export function getSignatures(contract: Contract): string[] {
0xCourtney marked this conversation as resolved.
Show resolved Hide resolved
return Object.keys(contract.interface.functions);
}

// returns a list of selectors for a contract
export function getSelectors(contract: Contract): string[] {
const signatures = getSignatures(contract);
return signatures.reduce((acc: string[], val: string) => {
acc.push(contract.interface.getSighash(val));
return acc;
}, []);
0xCourtney marked this conversation as resolved.
Show resolved Hide resolved
}

// returns a list of Facets for a contract
export function getFacets(contracts: Contract[]): Facet[] {
return contracts.map((contract) => {
return {
target: contract.address,
selectors: getSelectors(contract),
};
});
}

// returns true if the selector is found in the facets
export function selectorExistsInFacets(
selector: string,
facets: Facet[],
): boolean {
for (const facet of facets) {
if (facet.selectors.includes(selector)) return true;
}
return false;
0xCourtney marked this conversation as resolved.
Show resolved Hide resolved
}

// preview FacetCut which adds unregistered selectors
export async function addUnregisteredSelectors(
diamond: IDiamondReadable,
contracts: Contract[],
only: FacetFilter[] = [],
except: FacetFilter[] = [],
): Promise<FacetCut[]> {
validateFilters(only, except);

const diamondFacets: Facet[] = await diamond.facets();
const facets = getFacets(contracts);

let selectorsAdded = false;
let facetCuts: FacetCut[] = [];

// if facet selector is unregistered then it should be added to the diamond.
for (const facet of facets) {
for (const selector of facet.selectors) {
const target = facet.target;

if (
target !== diamond.address &&
selector.length > 0 &&
!selectorExistsInFacets(selector, diamondFacets) &&
selectorIsFiltered(only, except, target, selector)
) {
facetCuts.push(
printFacetCuts(facet.target, [selector], FacetCutAction.ADD),
);

selectorsAdded = true;
}
}
}

if (!selectorsAdded) {
throw new Error('No selectors were added to FacetCut');
}

return groupFacetCuts(facetCuts);
}

// preview FacetCut which replaces registered selectors with unregistered selectors
export async function replaceRegisteredSelectors(
diamond: IDiamondReadable,
contracts: Contract[],
only: FacetFilter[] = [],
except: FacetFilter[] = [],
): Promise<FacetCut[]> {
validateFilters(only, except);

const diamondFacets: Facet[] = await diamond.facets();
const facets = getFacets(contracts);

let selectorsReplaced = false;
let facetCuts: FacetCut[] = [];

// if a facet selector is registered with a different target address, the target will
// be replaced
for (const facet of facets) {
for (const selector of facet.selectors) {
const target = facet.target;
const oldTarget = await diamond.facetAddress(selector);

if (
target != oldTarget &&
target != AddressZero &&
target != diamond.address &&
selector.length > 0 &&
selectorExistsInFacets(selector, diamondFacets) &&
selectorIsFiltered(only, except, target, selector)
) {
facetCuts.push(
printFacetCuts(target, [selector], FacetCutAction.REPLACE),
);

selectorsReplaced = true;
}
}
}

if (!selectorsReplaced) {
throw new Error('No selectors were replaced in FacetCut');
Copy link
Member

Choose a reason for hiding this comment

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

The errors are thrown if all changes are skipped, but I think we'll want to throw if any change is skipped. Need to think about this though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would this be the result of using filters or something else?

}

return groupFacetCuts(facetCuts);
}

// preview FacetCut which removes registered selectors
export async function removeRegisteredSelectors(
diamond: IDiamondReadable,
contracts: Contract[],
only: FacetFilter[] = [],
except: FacetFilter[] = [],
): Promise<FacetCut[]> {
validateFilters(only, except);

const diamondFacets: Facet[] = await diamond.facets();
const facets = getFacets(contracts);

let selectorsRemoved = false;
let facetCuts: FacetCut[] = [];

// if a registered selector is not found in the facets then it should be removed
// from the diamond
for (const diamondFacet of diamondFacets) {
for (const selector of diamondFacet.selectors) {
const target = diamondFacet.target;

if (
target != AddressZero &&
target != diamond.address &&
selector.length > 0 &&
!selectorExistsInFacets(selector, facets) &&
selectorIsFiltered(only, except, AddressZero, selector)
) {
facetCuts.push(
printFacetCuts(AddressZero, [selector], FacetCutAction.REMOVE),
);

selectorsRemoved = true;
}
}
}

if (!selectorsRemoved) {
throw new Error('No selectors were removed from FacetCut');
}

return groupFacetCuts(facetCuts);
}

// preview a FacetCut which adds, replaces, or removes selectors, as needed
export async function previewFacetCut(
diamond: IDiamondReadable,
contracts: Contract[],
only: FacetFilter[][] = [[], [], []],
except: FacetFilter[][] = [[], [], []],
): Promise<FacetCut[]> {
let addFacetCuts: FacetCut[] = [];
let replaceFacetCuts: FacetCut[] = [];
let removeFacetCuts: FacetCut[] = [];

try {
addFacetCuts = await addUnregisteredSelectors(
diamond,
contracts,
only[0],
except[0],
);
} catch (error) {
console.log(`WARNING: ${(error as Error).message}`);
}

try {
replaceFacetCuts = await replaceRegisteredSelectors(
diamond,
contracts,
only[1],
except[1],
);
} catch (error) {
console.log(`WARNING: ${(error as Error).message}`);
}

try {
removeFacetCuts = await removeRegisteredSelectors(
diamond,
contracts,
only[2],
except[2],
);
} catch (error) {
console.log(`WARNING: ${(error as Error).message}`);
}

return groupFacetCuts([
...addFacetCuts,
...replaceFacetCuts,
...removeFacetCuts,
]);
}

// executes a DiamondCut using the provided FacetCut
export async function diamondCut(
diamond: IDiamondWritable,
facetCut: FacetCut[],
target: string = AddressZero,
data: string = '0x',
) {
(await diamond.diamondCut(facetCut, target, data)).wait(1);
Copy link
Member

Choose a reason for hiding this comment

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

How exactly does wait work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolves to the TransactionReceipt once the transaction has been included in the chain for confirms blocks. If confirms is 0, and the transaction has not been mined, null is returned.

It may be good to return the TransactionReceipt here. Otherwise, a user may not be able to detect errors or access the receipt, if needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

}

// groups facet cuts by target address and action type
export function groupFacetCuts(facetCuts: FacetCut[]): FacetCut[] {
const cuts = facetCuts.reduce((acc: FacetCut[], facetCut: FacetCut) => {
if (acc.length == 0) acc.push(facetCut);

let exists = false;

acc.forEach((_, i) => {
if (
acc[i].action == facetCut.action &&
acc[i].target == facetCut.target
) {
acc[i].selectors.push(...facetCut.selectors);
// removes duplicates, if there are any
acc[i].selectors = [...new Set(acc[i].selectors)];
exists = true;
}
});

// push facet cut if it does not already exist
if (!exists) acc.push(facetCut);

return acc;
}, []);

let cache: any = {};

// checks if selector is used multiple times, emits warning
cuts.forEach((cut) => {
cut.selectors.forEach((selector: string) => {
if (cache[selector]) {
console.log(
`WARNING: selector: ${selector}, target: ${cut.target} is defined in multiple cuts`,
);
} else {
cache[selector] = true;
}
});
});

return cuts;
}

export function printFacetCuts(
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't seem to print anything.

Copy link
Contributor Author

@0xCourtney 0xCourtney Dec 22, 2022

Choose a reason for hiding this comment

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

Maybe getFacetCut is more appropriate, the function only structures the FacetCut object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

target: string,
selectors: string[],
action: number = 0,
): FacetCut {
return {
target: target,
action: action,
selectors: selectors,
};
}
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './bn_conversion';
export * from './erc20_permit';
export * from './mocha_describe_filter';
export * from './sign_data';
export * from './diamond/utils';
2 changes: 2 additions & 0 deletions lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
"tsc-clean": "tsc --build --clean tsconfig.json"
},
"dependencies": {
"@ethersproject/constants": "^5.7.0",
"@ethersproject/contracts": "^5.7.0",
"eth-permit": "^0.1.10"
},
"files": [
Expand Down
Loading