Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
175 changes: 175 additions & 0 deletions docs/RewardsBehaviourChanges.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ const config: HardhatUserConfig = {
etherscan: {
// Use ARBISCAN_API_KEY for Arbitrum networks
// For mainnet Ethereum, use ETHERSCAN_API_KEY
apiKey: vars.has('ARBISCAN_API_KEY') ? vars.get('ARBISCAN_API_KEY') : '',
// Check both keystore (vars) and environment variable
apiKey: vars.has('ARBISCAN_API_KEY') ? vars.get('ARBISCAN_API_KEY') : (process.env.ARBISCAN_API_KEY ?? ''),
},
sourcify: {
enabled: false,
Expand Down
13 changes: 12 additions & 1 deletion packages/deployment/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import executeGovernanceTask from './tasks/execute-governance.js'
import grantRoleTask from './tasks/grant-role.js'
import listPendingTask from './tasks/list-pending-implementations.js'
import listRolesTask from './tasks/list-roles.js'
import { reoDisableTask, reoEnableTask, reoStatusTask } from './tasks/reo-tasks.js'
import resetForkTask from './tasks/reset-fork.js'
import revokeRoleTask from './tasks/revoke-role.js'
import verifyContractTask from './tasks/verify-contract.js'
Expand Down Expand Up @@ -50,7 +51,14 @@ function getDeployerKeyName(networkName: string): string {
}

/**
* Get accounts config for a network using configVariable for lazy resolution
* Get accounts config for a network.
*
* Uses configVariable for lazy resolution. If the key is not set (env var or keystore),
* read-only operations will still work but signing will fail with HHE7 error.
*
* To enable signing, set the key via:
* - Environment: export ARBITRUM_SEPOLIA_DEPLOYER_KEY=0x...
* - Keystore: npx hardhat keystore set ARBITRUM_SEPOLIA_DEPLOYER_KEY
*/
const getNetworkAccounts = (networkName: string) => {
return [configVariable(getDeployerKeyName(networkName))]
Expand All @@ -71,6 +79,9 @@ const config: HardhatUserConfig = {
grantRoleTask,
listPendingTask,
listRolesTask,
reoDisableTask,
reoEnableTask,
reoStatusTask,
resetForkTask,
revokeRoleTask,
verifyContractTask,
Expand Down
2 changes: 1 addition & 1 deletion packages/deployment/lib/abis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function loadAbi(artifactPath: string): Abi {
// and packages/contracts-test/tests/unit/rewards/rewards-interface.test.ts
export const IERC165_INTERFACE_ID = '0x01ffc9a7' as const
export const IISSUANCE_TARGET_INTERFACE_ID = '0xaee4dc43' as const
export const IREWARDS_MANAGER_INTERFACE_ID = '0xa0a2f219' as const
export const IREWARDS_MANAGER_INTERFACE_ID = '0x36b70adb' as const

export const REWARDS_MANAGER_ABI = loadAbi(
'@graphprotocol/interfaces/artifacts/contracts/contracts/rewards/IRewardsManager.sol/IRewardsManager.json',
Expand Down
144 changes: 104 additions & 40 deletions packages/deployment/lib/sync-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,14 +463,74 @@ async function syncContract(
// Get updated entry for formatProxyStatusLine
const updatedEntry = spec.proxy.addressBook.getEntry(spec.name)

// Heal/verify bytecode hash BEFORE checking codeChanged
// This ensures syncNotes are populated before formatProxyStatusLine
const pendingImpl = updatedEntry.pendingImplementation
const implAddress = pendingImpl?.address ?? updatedEntry.implementation
const implDeployment = pendingImpl
? pendingImpl.deployment
: spec.proxy.addressBook.getDeploymentMetadata(spec.name)

// Load artifact once and reuse across healing, codeChanged check, and hash comparison
const localArtifact = spec.proxy.artifact ? loadArtifactFromSource(spec.proxy.artifact) : undefined
const localHash = localArtifact?.deployedBytecode
? computeBytecodeHash(localArtifact.deployedBytecode)
: undefined

if (implAddress && spec.proxy.artifact) {
const storedHash = implDeployment?.bytecodeHash

if (localHash) {
// If no stored hash or hash mismatch, fetch on-chain bytecode to verify
if (!storedHash || storedHash !== localHash) {
try {
const onChainBytecode = await client.getCode({ address: implAddress as `0x${string}` })
if (onChainBytecode && onChainBytecode !== '0x') {
const onChainHash = computeBytecodeHash(onChainBytecode)

if (localHash === onChainHash) {
// Local matches on-chain - heal address book
if (!storedHash) {
syncNotes.push('hash verified')
} else {
syncNotes.push('hash healed')
}

// Update address book with verified hash
const healedMetadata = {
txHash: implDeployment?.txHash ?? '',
argsData: implDeployment?.argsData ?? '0x',
bytecodeHash: onChainHash,
...(implDeployment?.blockNumber && { blockNumber: implDeployment.blockNumber }),
...(implDeployment?.timestamp && { timestamp: implDeployment.timestamp }),
}
if (pendingImpl) {
spec.proxy.addressBook.setPendingDeploymentMetadata(spec.name, healedMetadata)
} else {
spec.proxy.addressBook.setImplementationDeploymentMetadata(spec.name, healedMetadata)
}
} else if (storedHash === onChainHash) {
// Stored hash matches on-chain, but local is different
syncNotes.push('local code changed')
} else {
// All three differ - complex case
syncNotes.push('impl state unclear')
}
}
} catch {
// Couldn't verify on-chain
syncNotes.push('impl unverified')
}
}
}
}

// Check if local bytecode differs from deployed (via bytecodeHash)
// If artifact exists but no bytecodeHash stored, assume code changed (untracked state)
// This uses the potentially-healed hash from above
let codeChanged = false
if (spec.proxy.artifact) {
const deploymentMetadata = spec.proxy.addressBook.getDeploymentMetadata(spec.name)
const localArtifact = loadArtifactFromSource(spec.proxy.artifact)
if (deploymentMetadata?.bytecodeHash && localArtifact?.deployedBytecode) {
const localHash = computeBytecodeHash(localArtifact.deployedBytecode)
if (deploymentMetadata?.bytecodeHash && localHash) {
codeChanged = localHash !== deploymentMetadata.bytecodeHash
} else if (localArtifact?.deployedBytecode) {
// No stored bytecodeHash but artifact exists - untracked/legacy state
Expand Down Expand Up @@ -507,32 +567,25 @@ async function syncContract(

if (!existing) {
// No existing record - create from artifact
// IMPORTANT: For proxy contracts, we only load the ABI, not bytecode
// The artifact is for the implementation, not the proxy itself
let abi: readonly unknown[] = []
let bytecode: `0x${string}` = '0x'
let deployedBytecode: `0x${string}` | undefined
if (spec.artifact) {
const artifact = loadArtifactFromSource(spec.artifact)
if (artifact?.abi) {
abi = artifact.abi
}
if (artifact?.bytecode) {
bytecode = artifact.bytecode as `0x${string}`
}
if (artifact?.deployedBytecode) {
deployedBytecode = artifact.deployedBytecode as `0x${string}`
}
}
await env.save(spec.name, {
address: spec.address as `0x${string}`,
abi: abi as typeof abi & readonly unknown[],
bytecode,
deployedBytecode,
bytecode: '0x' as `0x${string}`, // Don't store impl bytecode for proxy record
deployedBytecode: undefined,
argsData: '0x' as `0x${string}`,
metadata: '',
} as unknown as Parameters<typeof env.save>[1])
} else if (addressChanged) {
// Address changed - update address but preserve existing bytecode
// This handles the case where address book points to new address
// Address changed - update address and clear bytecode (proxy address changed)
let abi: readonly unknown[] = existing.abi as readonly unknown[]
// Update ABI from artifact if available (ABI doesn't affect change detection)
if (spec.artifact) {
Expand All @@ -544,10 +597,10 @@ async function syncContract(
await env.save(spec.name, {
address: spec.address as `0x${string}`,
abi: abi as typeof abi & readonly unknown[],
bytecode: existing.bytecode as `0x${string}`,
deployedBytecode: existing.deployedBytecode as `0x${string}`,
argsData: existing.argsData as `0x${string}`,
metadata: existing.metadata ?? '',
bytecode: '0x' as `0x${string}`, // Clear bytecode - proxy changed
deployedBytecode: undefined,
argsData: '0x' as `0x${string}`,
metadata: '',
} as unknown as Parameters<typeof env.save>[1])
}
// else: existing record with same address - do nothing, preserve rocketh's state
Expand Down Expand Up @@ -625,29 +678,29 @@ async function syncContract(
} as unknown as Parameters<typeof env.save>[1])
}

// Save implementation deployment record
// Pick pending or current - both have same structure (address + deployment metadata)
const pendingImpl = updatedEntry.pendingImplementation
const implAddress = pendingImpl?.address ?? updatedEntry.implementation
const implDeployment = pendingImpl
? pendingImpl.deployment
: spec.proxy.addressBook.getDeploymentMetadata(spec.name)

// Save implementation deployment record (if hash was verified/healed above)
if (implAddress) {
const storedHash = implDeployment?.bytecodeHash

// Only sync if stored hash matches local artifact
let hashMatches = false
if (storedHash && spec.proxy.artifact) {
const localArtifact = loadArtifactFromSource(spec.proxy.artifact)
if (localArtifact?.deployedBytecode) {
const localHash = computeBytecodeHash(localArtifact.deployedBytecode)
if (storedHash === localHash) {
hashMatches = true
} else {
syncNotes.push('impl outdated')
}
}

// Check if hash now matches after potential healing
if (storedHash && localHash) {
hashMatches = storedHash === localHash
}

// Clean up stale rocketh record if hash doesn't match
// Overwrite with empty bytecode to force deploy to create fresh
const existingImpl = env.getOrNull(`${spec.name}_Implementation`)
if (!hashMatches && existingImpl) {
// Overwrite stale record with empty bytecode - forces fresh deployment
await env.save(`${spec.name}_Implementation`, {
address: existingImpl.address,
abi: existingImpl.abi,
bytecode: '0x' as `0x${string}`,
deployedBytecode: undefined,
argsData: '0x' as `0x${string}`,
metadata: '',
} as unknown as Parameters<typeof env.save>[1])
}

if (hashMatches) {
Expand Down Expand Up @@ -875,6 +928,17 @@ export async function getContractStatusLine(
return { line: `✓ ${contractName} @ ${formatAddress(entry.address)}`, exists: true }
}

// If no client available, show address book status without on-chain verification
if (!client) {
if (meta?.proxyType && entry.implementation) {
return {
line: `? ${contractName} @ ${formatAddress(entry.address)} → ${formatAddress(entry.implementation)} (no on-chain check)`,
exists: true,
}
}
return { line: `? ${contractName} @ ${formatAddress(entry.address)} (no on-chain check)`, exists: true }
}

// Check if code exists on-chain
const code = await client.getCode({ address: entry.address as `0x${string}` })
if (!code || code === '0x') {
Expand Down
54 changes: 54 additions & 0 deletions packages/deployment/scripts/check-bytecode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { createPublicClient, http } from 'viem'

import { loadSubgraphServiceArtifact } from '../lib/artifact-loaders.js'
import { computeBytecodeHash } from '../lib/bytecode-utils.js'
import { graph } from '../rocketh/deploy.js'

async function main() {
const chainId = 421614 // arbitrumSepolia

// Get address book
const addressBook = graph.getSubgraphServiceAddressBook(chainId)
const entry = addressBook.getEntry('SubgraphService')
const deploymentMetadata = addressBook.getDeploymentMetadata('SubgraphService')

console.log('\n📋 SubgraphService Bytecode Analysis\n')
console.log('Proxy address:', entry.address)
console.log('Current implementation:', entry.implementation)
console.log('Pending implementation:', entry.pendingImplementation?.address ?? 'none')

// Get local artifact
const artifact = loadSubgraphServiceArtifact('SubgraphService')
const localHash = computeBytecodeHash(artifact.deployedBytecode ?? '0x')
console.log('\nLocal artifact bytecode hash:', localHash)

// Get address book stored hash
console.log('Address book stored hash:', deploymentMetadata?.bytecodeHash ?? '(none)')

// Get on-chain bytecode
const client = createPublicClient({
transport: http('https://sepolia-rollup.arbitrum.io/rpc'),
})

const onChainBytecode = await client.getCode({
address: entry.implementation as `0x${string}`,
})

if (onChainBytecode && onChainBytecode !== '0x') {
const onChainHash = computeBytecodeHash(onChainBytecode)
console.log('On-chain implementation hash:', onChainHash)

console.log('\n🔍 Comparison:')
console.log(
'Local vs Address Book:',
localHash === (deploymentMetadata?.bytecodeHash ?? '') ? '✓ MATCH' : '✗ DIFFERENT',
)
console.log('Local vs On-chain:', localHash === onChainHash ? '✓ MATCH' : '✗ DIFFERENT')
console.log(
'Address Book vs On-chain:',
(deploymentMetadata?.bytecodeHash ?? '') === onChainHash ? '✓ MATCH' : '✗ DIFFERENT (or missing)',
)
}
}

main().catch(console.error)
34 changes: 34 additions & 0 deletions packages/deployment/scripts/check-rocketh-bytecode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { readFileSync } from 'fs'

import { loadSubgraphServiceArtifact } from '../lib/artifact-loaders.js'
import { computeBytecodeHash } from '../lib/bytecode-utils.js'

async function main() {
console.log('\n📋 Rocketh vs Local Artifact Comparison\n')

// Get local artifact
const artifact = loadSubgraphServiceArtifact('SubgraphService')
const localHash = computeBytecodeHash(artifact.deployedBytecode ?? '0x')
console.log('Local artifact hash:', localHash)

// Check rocketh stored bytecode
try {
const rockethPath = '.rocketh/deployments/arbitrumSepolia/SubgraphService_Implementation.json'
const rockethData = JSON.parse(readFileSync(rockethPath, 'utf-8'))

if (rockethData.deployedBytecode) {
const rockethHash = computeBytecodeHash(rockethData.deployedBytecode)
console.log('Rocketh stored hash:', rockethHash)
console.log(
'\nComparison:',
localHash === rockethHash ? '✓ MATCH (deploy will skip)' : '✗ DIFFERENT (deploy will redeploy)',
)
} else {
console.log('Rocketh stored hash: (no deployedBytecode)')
}
} catch {
console.log('Rocketh record:', 'not found')
}
}

main().catch(console.error)
Loading
Loading