-
Notifications
You must be signed in to change notification settings - Fork 90
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
base: master
Are you sure you want to change the base?
Changes from 22 commits
a7edda4
420964c
047964e
9d3a62d
9d3b060
20bf78e
b2eda57
398ff37
f9a6c2c
45b2dc4
3d9d456
e702c36
5f34d05
1aa7db8
f00863e
c8195bc
f43c14a
c93c0a5
e5d19a2
dfac641
669a620
cef5a3f
ce286c6
a2a3c19
c7df310
7d6598e
9d92ea0
a9b12a5
f4c211b
dbf11d5
871f868
54da442
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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[]; | ||
} | ||
|
||
// returns true if the selector is found in the only or exclude filters | ||
export function selectorIsFiltered( | ||
only: FacetFilter[], | ||
exclude: FacetFilter[], | ||
contract: string, | ||
selector: string, | ||
): boolean { | ||
if (only.length > 0) { | ||
// include selectors found in only, exclude all others | ||
return includes(only, contract, selector); | ||
} | ||
|
||
if (exclude.length > 0) { | ||
// exclude selectors found in exclude, include all others | ||
return !includes(exclude, contract, selector); | ||
} | ||
|
||
// if neither only or exclude 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 exclude filters do not contain the same contract | ||
export function validateFilters(only: FacetFilter[], exclude: FacetFilter[]) { | ||
if (only.length > 0 && exclude.length > 0) { | ||
for (const onlyFilter of only) { | ||
for (const excludeFilter of exclude) { | ||
if (onlyFilter.contract === excludeFilter.contract) { | ||
throw new Error( | ||
'only and exclude filters cannot contain the same contract', | ||
); | ||
} | ||
} | ||
} | ||
} | ||
} |
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[] = [], | ||
exclude: FacetFilter[] = [], | ||
): Promise<FacetCut[]> { | ||
validateFilters(only, exclude); | ||
|
||
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, exclude, 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[] = [], | ||
exclude: FacetFilter[] = [], | ||
): Promise<FacetCut[]> { | ||
validateFilters(only, exclude); | ||
|
||
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, exclude, target, selector) | ||
) { | ||
facetCuts.push( | ||
printFacetCuts(target, [selector], FacetCutAction.REPLACE), | ||
); | ||
|
||
selectorsReplaced = true; | ||
} | ||
} | ||
} | ||
|
||
if (!selectorsReplaced) { | ||
throw new Error('No selectors were replaced in FacetCut'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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[] = [], | ||
exclude: FacetFilter[] = [], | ||
): Promise<FacetCut[]> { | ||
validateFilters(only, exclude); | ||
|
||
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, exclude, 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[][] = [[], [], []], | ||
exclude: FacetFilter[][] = [[], [], []], | ||
): Promise<FacetCut[]> { | ||
let addFacetCuts: FacetCut[] = []; | ||
let replaceFacetCuts: FacetCut[] = []; | ||
let removeFacetCuts: FacetCut[] = []; | ||
|
||
try { | ||
addFacetCuts = await addUnregisteredSelectors( | ||
diamond, | ||
contracts, | ||
only[0], | ||
exclude[0], | ||
); | ||
} catch (error) { | ||
console.log(`WARNING: ${(error as Error).message}`); | ||
} | ||
|
||
try { | ||
replaceFacetCuts = await replaceRegisteredSelectors( | ||
diamond, | ||
contracts, | ||
only[1], | ||
exclude[1], | ||
); | ||
} catch (error) { | ||
console.log(`WARNING: ${(error as Error).message}`); | ||
} | ||
|
||
try { | ||
removeFacetCuts = await removeRegisteredSelectors( | ||
diamond, | ||
contracts, | ||
only[2], | ||
exclude[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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How exactly does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't seem to print anything. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
}; | ||
} |
There was a problem hiding this comment.
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 theFacetCutAction
types. Would it not be better to include the action in theFacetFilter
type?There was a problem hiding this comment.
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
andexcept
parameters withfilter
in the add/replace/remove API, then add atype
field toFacetFilter
. The newFacetFilter
could look something like this:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
54da442