Skip to content

Commit 255c5b2

Browse files
committed
feat: record core facet deployments in deployment history
- Add txHash field to OnChainContract interface - Create recordCoreFacetDeployments function to track core facets - Match deployed facets against artifacts using bytecode comparison - Record core facet deployment info with diamond proxy as sender - Add comprehensive tests for core facet recording and verification
1 parent 4c3a8c4 commit 255c5b2

3 files changed

Lines changed: 142 additions & 4 deletions

File tree

src/commands/deploy.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Signer, ZeroAddress, ethers } from 'ethers'
2-
import { OnChainContract, Target, clearDeploymentRecorder, clearNonceCache, deployContract, deployContract3, execContractMethod, getContractAt, getDeploymentRecorderData, saveDeploymentInfo, setupTarget, setupWallet } from '../shared/chain.js'
2+
import { OnChainContract, Target, clearDeploymentRecorder, clearNonceCache, deployContract, deployContract3, execContractMethod, getContractAt, getDeploymentRecorderData, recordCoreFacetDeployments, saveDeploymentInfo, setupTarget, setupWallet } from '../shared/chain.js'
33
import { Context, getContext } from '../shared/context.js'
44
import { FacetCut, FacetCutAction, getFinalizedFacetCuts, resolveClean, resolveUpgrade } from '../shared/diamond.js'
55
import { $, loadJson, saveJson } from '../shared/fs.js'
@@ -282,7 +282,19 @@ export const command = () =>
282282
}
283283
const diamond = await deployContract3(ctx, 'DiamondProxy', signer, salt32bytes, await signer.getAddress())
284284
info(` ...deployed at: ${diamond.address}`)
285-
return await getContractAt(ctx, 'IDiamondProxy', signer, diamond.address)
285+
286+
const proxyInterface = await getContractAt(ctx, 'IDiamondProxy', signer, diamond.address)
287+
288+
info('Recording core facet deployments...')
289+
await recordCoreFacetDeployments(
290+
ctx,
291+
signer,
292+
diamond.address,
293+
ctx.config.diamond.coreFacets,
294+
diamond.txHash!
295+
)
296+
297+
return proxyInterface
286298
}
287299

288300

src/shared/chain.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ export interface OnChainContract {
209209
artifact: ContractArtifact
210210
address: string
211211
contract: Contract
212+
txHash?: string
212213
}
213214

214215
export const getContractAt = async (ctx: Context, name: string, signer: Signer, address: string): Promise<OnChainContract> => {
@@ -485,7 +486,11 @@ export const deployContract3 = async (
485486
},
486487
})
487488

488-
return getContractAt(ctx, name, signer, address)
489+
const contract = await getContractAt(ctx, name, signer, address)
490+
return {
491+
...contract,
492+
txHash: receipt.hash,
493+
}
489494
} catch (err: any) {
490495
return error(`Failed to deploy ${name}: ${err.message}}`)
491496
}
@@ -629,6 +634,74 @@ interface ContractArtifactPath {
629634
fullyQualifiedName: string
630635
}
631636

637+
/**
638+
* Record core facet deployments to the deployment recorder.
639+
*
640+
* Queries the diamond proxy for deployed facets and matches them against
641+
* core facet artifacts, adding deployment records for each matched core facet.
642+
*/
643+
export const recordCoreFacetDeployments = async (
644+
ctx: Context,
645+
signer: Signer,
646+
proxyAddress: string,
647+
coreFacetNames: string[],
648+
diamondDeploymentTxHash: string
649+
): Promise<void> => {
650+
trace(`Recording core facet deployments for diamond at ${proxyAddress} ...`)
651+
652+
// Get the diamond proxy interface to query facets
653+
const proxy = await getContractAt(ctx, 'IDiamondProxy', signer, proxyAddress)
654+
655+
// Query deployed facets
656+
const deployedFacets = await getContractValue<{ facetAddress: string; functionSelectors: string[] }[]>(proxy, 'facets', [])
657+
trace(` Found ${deployedFacets.length} deployed facets`)
658+
659+
// Load core facet artifacts
660+
const coreFacetArtifacts: Record<string, ContractArtifact> = {}
661+
coreFacetNames.forEach(name => {
662+
coreFacetArtifacts[name] = loadContractArtifact(ctx, name)
663+
})
664+
665+
// Fetch bytecode for deployed facets and match with core facets
666+
const bytecodeFetcher = new BytecodeFetcher(signer)
667+
const facetAddresses = new Set<string>()
668+
669+
for (const facet of deployedFacets) {
670+
const facetAddress = facet.facetAddress
671+
if (!facetAddresses.has(facetAddress)) {
672+
facetAddresses.add(facetAddress)
673+
}
674+
}
675+
676+
trace(` ${facetAddresses.size} unique facet addresses found`)
677+
678+
for (const facetAddress of facetAddresses) {
679+
const deployedBytecode = await bytecodeFetcher.getBytecode(facetAddress)
680+
681+
// Find matching core facet artifact
682+
for (const [name, artifact] of Object.entries(coreFacetArtifacts)) {
683+
if (artifact.deployedBytecode === deployedBytecode) {
684+
trace(` Matched facet at ${facetAddress} with core facet ${name}`)
685+
686+
deploymentRecorder.push({
687+
name,
688+
fullyQualifiedName: artifact.fullyQualifiedName,
689+
sender: proxyAddress,
690+
txHash: diamondDeploymentTxHash,
691+
onChain: {
692+
address: facetAddress,
693+
constructorArgs: [],
694+
},
695+
})
696+
697+
break
698+
}
699+
}
700+
}
701+
702+
trace(` ...done recording core facet deployments`)
703+
}
704+
632705
const getAllContractArtifactPaths = (ctx: Context): ContractArtifactPath[] => {
633706
const files = glob.sync(`${ctx.artifactsPath}/**/*.json`) as string[]
634707

@@ -651,7 +724,7 @@ const getAllContractArtifactPaths = (ctx: Context): ContractArtifactPath[] => {
651724
default:
652725
error(`Unknown artifacts format: ${ctx.config.artifacts.format}`)
653726
}
654-
727+
655728
return {
656729
jsonFilePath,
657730
fullyQualifiedName,

test/common-deploy-steps.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,23 @@ export const addDeployTestSteps = ({
4545
expect(obj2).to.have.property('txHash')
4646
expect(obj2).to.have.property('onChain')
4747
expect(obj2.onChain).to.have.property('address')
48+
49+
const coreFacets = ['DiamondCutFacet', 'DiamondLoupeFacet', 'OwnershipFacet']
50+
coreFacets.forEach(facetName => {
51+
const facet = get(json, 'local.contracts', []).find((a: any) => a.name === facetName) as any
52+
expect(facet).to.exist
53+
expect(facet).to.have.property('name')
54+
expect(facet.name).to.equal(facetName)
55+
expect(facet).to.have.property('fullyQualifiedName')
56+
expect(facet).to.have.property('sender')
57+
expect(facet.sender).to.equal(obj.onChain.address)
58+
expect(facet).to.have.property('txHash')
59+
expect(facet.txHash).to.equal(obj.txHash)
60+
expect(facet).to.have.property('onChain')
61+
expect(facet.onChain).to.have.property('address')
62+
expect(facet.onChain).to.have.property('constructorArgs')
63+
expect(facet.onChain.constructorArgs).to.be.an('array').that.is.empty
64+
})
4865
})
4966

5067
it('and the facets really are deployed', async () => {
@@ -59,6 +76,24 @@ export const addDeployTestSteps = ({
5976
expect(contract.owner()).to.eventually.eq(walletAddress)
6077
})
6178

79+
it('and records core facets with addresses matching the diamond query', async () => {
80+
const filePath = join(cwd, 'gemforge.deployments.json')
81+
const json = loadJsonFile(filePath)
82+
83+
const { contract } = await loadDiamondContract(cwd)
84+
const deployedFacets = await contract.facets()
85+
86+
const coreFacets = ['DiamondCutFacet', 'DiamondLoupeFacet', 'OwnershipFacet']
87+
coreFacets.forEach(facetName => {
88+
const facetRecord = get(json, 'local.contracts', []).find((a: any) => a.name === facetName) as any
89+
expect(facetRecord).to.exist
90+
91+
const deployedFacet = deployedFacets.find((f: any) => f.facetAddress.toLowerCase() === facetRecord.onChain.address.toLowerCase())
92+
expect(deployedFacet).to.exist
93+
expect(deployedFacet.functionSelectors.length).to.be.greaterThan(0)
94+
})
95+
})
96+
6297
it('and can handle additions and replacements in a facet', async () => {
6398
const filePath = join(cwd, 'gemforge.deployments.json')
6499
const jsonOld = loadJsonFile(filePath)
@@ -226,6 +261,15 @@ export const addDeployTestSteps = ({
226261
expect(newAddr.toLowerCase()).to.not.equal(oldAddr.toLowerCase())
227262
})
228263

264+
const coreFacets = ['DiamondCutFacet', 'DiamondLoupeFacet', 'OwnershipFacet']
265+
coreFacets.forEach((name) => {
266+
const oldFacet = get(jsonOld, 'local.contracts', []).find((a: any) => a.name === name) as any
267+
const newFacet = get(jsonNew, 'local.contracts', []).find((a: any) => a.name === name) as any
268+
expect(oldFacet).to.exist
269+
expect(newFacet).to.exist
270+
expect(newFacet.onChain.address.toLowerCase()).to.not.equal(oldFacet.onChain.address.toLowerCase())
271+
})
272+
229273
const { contract } = await loadDiamondContract(cwd)
230274
const n = await contract.getInt1()
231275
expect(n.toString()).to.equal('0')
@@ -255,6 +299,15 @@ export const addDeployTestSteps = ({
255299
expect(newAddr.toLowerCase()).to.equal(oldAddr.toLowerCase())
256300
})
257301

302+
const coreFacets = ['DiamondCutFacet', 'DiamondLoupeFacet', 'OwnershipFacet']
303+
coreFacets.forEach((name) => {
304+
const oldFacet = get(jsonOld, 'local.contracts', []).find((a: any) => a.name === name) as any
305+
const newFacet = get(jsonNew, 'local.contracts', []).find((a: any) => a.name === name) as any
306+
expect(oldFacet).to.exist
307+
expect(newFacet).to.exist
308+
expect(newFacet.onChain.address.toLowerCase()).to.equal(oldFacet.onChain.address.toLowerCase())
309+
})
310+
258311
const n = await contract.getInt1()
259312
expect(n.toString()).to.equal('2') // still the same as before!
260313
})

0 commit comments

Comments
 (0)