Skip to content

Commit

Permalink
fix(ex): make executor most robust to errors and cancelled bundles
Browse files Browse the repository at this point in the history
  • Loading branch information
sam-goldman committed Nov 19, 2022
1 parent 4d7ac72 commit fb1168f
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 149 deletions.
7 changes: 7 additions & 0 deletions .changeset/selfish-islands-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@chugsplash/core': patch
'@chugsplash/executor': patch
'@chugsplash/plugins': patch
---

Make executor most robust to errors and cancelled bundles. Ensure that executor receives payment.
85 changes: 85 additions & 0 deletions packages/core/src/fund.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { ChugSplashManagerABI } from '@chugsplash/contracts'
import { ethers } from 'ethers'

import { getChugSplashManagerProxyAddress } from './utils'
import { ChugSplashConfig } from './config/types'

/**
* Gets the amount ETH in the ChugSplashManager that can be used to execute a deployment.
* This equals the ChugSplashManager's balance minus the total debt owed to executors.
*/
export const getOwnerBalanceInChugSplashManager = async (
provider: ethers.providers.JsonRpcProvider,
projectName: string
): Promise<ethers.BigNumber> => {
const ChugSplashManager = new ethers.Contract(
getChugSplashManagerProxyAddress(projectName),
ChugSplashManagerABI,
provider
)

const managerBalance = await provider.getBalance(ChugSplashManager.address)
const totalDebt = await ChugSplashManager.totalDebt()
return managerBalance.sub(totalDebt)
}

/**
* Gets the minimum amount that must be sent to the ChugSplashManager in order to execute the
* ChugSplash config. If this function returns zero, then there is already a sufficient amount of
* funds.
*
* @param provider JSON RPC provider.
* @param parsedConfig Parsed ChugSplash config.
* @returns The minimum amount to send to the ChugSplashManager in order to execute the config
* (denominated in wei).
*/
export const getExecutionAmountToSend = async (
provider: ethers.providers.JsonRpcProvider,
parsedConfig: ChugSplashConfig
): Promise<ethers.BigNumber> => {
const totalExecutionAmount = await simulateExecution(provider, parsedConfig)
const availableExecutionAmount = await getOwnerBalanceInChugSplashManager(
provider,
parsedConfig.options.projectName
)
const executionAmount = totalExecutionAmount.sub(availableExecutionAmount)
return executionAmount.gt(0) ? executionAmount : ethers.BigNumber.from(0)
}

export const simulateExecution = async (
provider: ethers.providers.JsonRpcProvider,
parsedConfig: ChugSplashConfig
) => {
provider
parsedConfig

// TODO
return ethers.utils.parseEther('0.25')
}

/**
* Returns the amount to send to the ChugSplashManager to execute a bundle, plus a buffer in case
* the gas price increases during execution. If this returns zero, there is already a sufficient
* amount of funds in the ChugSplashManager.
*
* @param provider JSON RPC provider.
* @param parsedConfig Parsed ChugSplash config.
* @returns The amount required to fund a bundle, plus a buffer. Denominated in wei.
*/
export const getExecutionAmountToSendPlusBuffer = async (
provider: ethers.providers.JsonRpcProvider,
parsedConfig: ChugSplashConfig
) => {
const executionAmount = await getExecutionAmountToSend(provider, parsedConfig)
return executionAmount.mul(15).div(10)
}

export const hasSufficientFundsForExecution = async (
provider: ethers.providers.JsonRpcProvider,
parsedConfig: ChugSplashConfig
): Promise<boolean> => {
// Get the amount of funds that must be sent to the ChugSplashManager in order to execute the
// bundle.
const executionAmount = await getExecutionAmountToSend(provider, parsedConfig)
return executionAmount.eq(0)
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './actions'
export * from './config'
export * from './languages'
export * from './utils'
export * from './fund'
10 changes: 10 additions & 0 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,13 @@ export const displayDeploymentTable = (
console.table(deployments)
}
}

export const claimExecutorPayment = async (
executor: Signer,
ChugSplashManager: Contract
) => {
const executorDebt = await ChugSplashManager.debt(await executor.getAddress())
if (executorDebt.gt(0)) {
await (await ChugSplashManager.claimExecutorPayment()).wait()
}
}
191 changes: 119 additions & 72 deletions packages/executor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import {
ChugSplashRegistryABI,
CHUGSPLASH_REGISTRY_PROXY_ADDRESS,
} from '@chugsplash/contracts'
import {
claimExecutorPayment,
hasSufficientFundsForExecution,
} from '@chugsplash/core'
import { getChainId } from '@eth-optimism/core-utils'
import * as Amplitude from '@amplitude/node'

Expand All @@ -22,8 +26,9 @@ type Options = {
type Metrics = {}

type State = {
events: ethers.Event[]
registry: ethers.Contract
wallet: ethers.Wallet
provider: ethers.providers.JsonRpcProvider
lastBlockNumber: number
amplitudeClient: Amplitude.NodeClient
}
Expand Down Expand Up @@ -66,111 +71,153 @@ export class ChugSplashExecutor extends BaseServiceV2<Options, Metrics, State> {
}

const reg = CHUGSPLASH_REGISTRY_PROXY_ADDRESS
const provider = ethers.getDefaultProvider(this.options.network)
this.state.provider = new ethers.providers.JsonRpcProvider(
this.options.network
)
this.state.registry = new ethers.Contract(
reg,
ChugSplashRegistryABI,
provider
this.state.provider
)
this.state.lastBlockNumber = -1
this.state.wallet = new ethers.Wallet(this.options.privateKey, provider)
this.state.events = []
}

async main() {
// Find all active upgrades that have not yet been executed in blocks after the stored hash
const approvalAnnouncementEvents = await this.state.registry.queryFilter(
const wallet = new ethers.Wallet(
this.options.privateKey,
this.state.provider
)

const latestBlockNumber = await this.state.provider.getBlockNumber()

// Get approval events in blocks after the stored block number
const newApprovalEvents = await this.state.registry.queryFilter(
this.state.registry.filters.EventAnnounced('ChugSplashBundleApproved'),
this.state.lastBlockNumber + 1
this.state.lastBlockNumber + 1,
latestBlockNumber
)

// Concatenate the new approval events to the array
this.state.events = this.state.events.concat(newApprovalEvents)

// store last block number
this.state.lastBlockNumber = latestBlockNumber

// If none found, return
if (approvalAnnouncementEvents.length === 0) {
if (this.state.events.length === 0) {
this.logger.info('no events found')
return
}

this.logger.info(`${approvalAnnouncementEvents.length} events found`)
this.logger.info(
`total number of events: ${this.state.events.length}. new events: ${newApprovalEvents.length}`
)

// store last block number
this.state.lastBlockNumber = approvalAnnouncementEvents.at(-1).blockNumber
const eventsCopy = this.state.events.slice()

// execute all approved bundles
for (const approvalAnnouncementEvent of approvalAnnouncementEvents) {
for (const approvalAnnouncementEvent of eventsCopy) {
// Remove the current event from the front of the events array and put it at the end
this.state.events.shift()
this.state.events.push(approvalAnnouncementEvent)

// fetch manager for relevant project
const signer = this.state.wallet
const manager = new ethers.Contract(
approvalAnnouncementEvent.args.manager,
ChugSplashManagerABI,
signer
wallet
)

// get active bundle id for this project
const activeBundleId = await manager.activeBundleId()
if (activeBundleId === ethers.constants.HashZero) {
this.logger.error(`Error: No active bundle id found in manager`)
continue
}

// get proposal event and compile
const proposalEvents = await manager.queryFilter(
manager.filters.ChugSplashBundleProposed(activeBundleId)
)
const proposalEvent = proposalEvents[0]
const { bundle, canonicalConfig } = await compileRemoteBundle(
hre,
proposalEvent.args.configUri
)
if (activeBundleId !== ethers.constants.HashZero) {
// Retrieve the corresponding proposal event to get the config URI.
const [proposalEvent] = await manager.queryFilter(
manager.filters.ChugSplashBundleProposed(activeBundleId)
)

// ensure compiled bundle matches proposed bundle
if (bundle.root !== proposalEvent.args.bundleRoot) {
// log error and continue
this.logger.error(
'Error: Compiled bundle root does not match proposal event bundle root',
canonicalConfig.options
// Compile the bundle using the config URI.
const { bundle, canonicalConfig } = await compileRemoteBundle(
hre,
proposalEvent.args.configUri
)
continue
}

// execute bundle
try {
await hre.run('chugsplash-execute', {
chugSplashManager: manager,
bundleId: activeBundleId,
bundle,
parsedConfig: canonicalConfig,
executor: signer,
hide: false,
})
this.logger.info('Successfully executed')
} catch (e) {
// log error and continue
this.logger.error('Error: execution error', e, canonicalConfig.options)
continue
}
// ensure compiled bundle matches proposed bundle
if (bundle.root !== proposalEvent.args.bundleRoot) {
// We cannot execute this bundle, so we remove it from the events array.
this.state.events.pop()

// log error and continue
this.logger.error(
'Error: Compiled bundle root does not match proposal event bundle root',
canonicalConfig.options
)
continue
}

// verify on etherscan
try {
if ((await getChainId(this.state.wallet.provider)) !== 31337) {
await verifyChugSplashConfig(hre, proposalEvent.args.configUri)
this.logger.info('Successfully verified')
if (
await hasSufficientFundsForExecution(
this.state.provider,
canonicalConfig
)
) {
// execute bundle
try {
await hre.run('chugsplash-execute', {
chugSplashManager: manager,
bundleId: activeBundleId,
bundle,
parsedConfig: canonicalConfig,
executor: wallet,
hide: false,
})
this.logger.info('Successfully executed')
} catch (e) {
// log error and continue
this.logger.error(
'Error: execution error',
e,
canonicalConfig.options
)
continue
}

// verify on etherscan
try {
if ((await getChainId(this.state.provider)) !== 31337) {
await verifyChugSplashConfig(hre, proposalEvent.args.configUri)
this.logger.info('Successfully verified')
}
} catch (e) {
this.logger.error(
'Error: verification error',
e,
canonicalConfig.options
)
}

if (this.options.amplitudeKey !== 'disabled') {
this.state.amplitudeClient.logEvent({
event_type: 'ChugSplash Executed',
user_id: canonicalConfig.options.projectOwner,
event_properties: {
projectName: canonicalConfig.options.projectName,
},
})
}
} else {
// Continue to the next bundle if there is an insufficient amount of funds in the
// ChugSplashManager.
continue
}
} catch (e) {
this.logger.error(
'Error: verification error',
e,
canonicalConfig.options
)
}

if (this.options.amplitudeKey !== 'disabled') {
this.state.amplitudeClient.logEvent({
event_type: 'ChugSplash Executed',
user_id: canonicalConfig.options.projectOwner,
event_properties: {
projectName: canonicalConfig.options.projectName,
},
})
}
// Withdraw any debt owed to the executor.
await claimExecutorPayment(wallet, manager)

// Remove the current event from the events array.
this.state.events.pop()
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions packages/plugins/src/hardhat/deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
ChugSplashActionBundle,
computeBundleId,
getChugSplashManager,
claimExecutorPayment,
getExecutionAmountToSendPlusBuffer
} from '@chugsplash/core'
import { getChainId } from '@eth-optimism/core-utils'
import { HardhatRuntimeEnvironment } from 'hardhat/types'
Expand All @@ -33,7 +35,6 @@ import {
monitorTask,
TASK_CHUGSPLASH_VERIFY_BUNDLE,
} from './tasks'
import { getExecutionAmountPlusBuffer } from './fund'

/**
* TODO
Expand Down Expand Up @@ -178,8 +179,8 @@ export const deployChugSplashConfig = async (
if (currBundleStatus === ChugSplashBundleStatus.PROPOSED) {
spinner.start('Funding the deployment...')
// Get the amount necessary to fund the deployment.
const executionAmountPlusBuffer = await getExecutionAmountPlusBuffer(
hre,
const executionAmountPlusBuffer = await getExecutionAmountToSendPlusBuffer(
hre.ethers.provider,
parsedConfig
)
// Approve and fund the deployment.
Expand Down Expand Up @@ -220,6 +221,7 @@ export const deployChugSplashConfig = async (
},
hre
)
await claimExecutorPayment(signer, ChugSplashManager)
}

spinner.succeed(`${parsedConfig.options.projectName} deployed!`)
Expand Down
Loading

0 comments on commit fb1168f

Please sign in to comment.