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

Make the same hotfix proposal executable with unique salts #2357

Merged
merged 9 commits into from
Jan 7, 2020
9 changes: 6 additions & 3 deletions packages/celotool/src/cmds/deploy/upgrade/hotfix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import { newKit } from '@celo/contractkit'
import {
hotfixToHash,
ProposalBuilder,
proposalToHash,
proposalToJSON,
} from '@celo/contractkit/lib/governance/proposals'
import { privateKeyToAddress } from '@celo/utils/lib/address'
import { concurrentMap } from '@celo/utils/lib/async'
import { randomBytes } from 'crypto'
import { getFornoUrl } from 'src/lib/endpoints'
import { envVar, fetchEnv } from 'src/lib/env-utils'
import { AccountType, getPrivateKeysFor } from 'src/lib/generate_utils'
Expand Down Expand Up @@ -78,7 +79,9 @@ export const handler = async (argv: EthstatsArgv) => {
console.error('\nPlease see examples in hotfix.ts and add transactions')
process.exit(1)
}
const proposalHash = proposalToHash(kit, proposal)

const salt = randomBytes(32)
Copy link
Contributor

Choose a reason for hiding this comment

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

should this be logged in case something goes wrong?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ditto

const proposalHash = hotfixToHash(kit, proposal, salt)

// If your proposal is just made of Celo Registry contract methods, you can print it out
console.info('Proposal: ', await proposalToJSON(kit, proposal))
Expand Down Expand Up @@ -119,7 +122,7 @@ export const handler = async (argv: EthstatsArgv) => {
throw new Error()
}
console.info('\nExecute the hotfix')
await governance.executeHotfix(proposal).sendAndWaitForReceipt({ from: addresses[0] })
await governance.executeHotfix(proposal, salt).sendAndWaitForReceipt({ from: addresses[0] })

hotfixRecord = await governance.getHotfixRecord(proposalHash)
console.info('\nHotfix Record: ', hotfixRecord)
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/commands/governance/executehotfix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default class ExecuteHotfix extends BaseCommand {
...BaseCommand.flags,
from: Flags.address({ required: true, description: "Executors's address" }),
jsonTransactions: flags.string({ required: true, description: 'Path to json transactions' }),
salt: flags.string({ required: true, description: 'Secret salt associated with hotfix' }),
}

static examples = []
Expand All @@ -20,7 +21,9 @@ export default class ExecuteHotfix extends BaseCommand {

const governance = await this.kit.contracts.getGovernance()
const hotfix = await buildProposalFromJsonFile(this.kit, res.flags.jsonTransactions)
const tx = governance.executeHotfix(hotfix)

const saltBuff = Buffer.from(res.flags.salt, 'hex')
const tx = governance.executeHotfix(hotfix, saltBuff)
await displaySendTx('executeHotfixTx', tx, { from: res.flags.from })
}
}
15 changes: 7 additions & 8 deletions packages/contractkit/src/governance/proposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,18 @@ import { obtainKitContractDetails } from '../explorer/base'
import { BlockExplorer } from '../explorer/block-explorer'
import { ABI as GovernanceABI } from '../generated/Governance'
import { ContractKit } from '../kit'
import { getAbiTypes } from '../utils/web3-utils'
import { CeloTransactionObject, valueToString } from '../wrappers/BaseWrapper'
import { GovernanceWrapper, Proposal, ProposalTransaction } from '../wrappers/Governance'
import { hotfixToParams, Proposal, ProposalTransaction } from '../wrappers/Governance'
import { setImplementationOnProxy } from './proxy'

export const PROPOSE_PARAM_ABI_TYPES = (GovernanceABI.find(
(abiEntry) => abiEntry.name! === 'propose'
)!.inputs! as Array<{ type: string }>).map((abiInput) => abiInput.type)
export const HOTFIX_PARAM_ABI_TYPES = getAbiTypes(GovernanceABI as any, 'executeHotfix')

export const proposalToEncodedParams = (kit: ContractKit, proposal: Proposal) =>
kit.web3.eth.abi.encodeParameters(PROPOSE_PARAM_ABI_TYPES, GovernanceWrapper.toParams(proposal))
export const hotfixToEncodedParams = (kit: ContractKit, proposal: Proposal, salt: Buffer) =>
kit.web3.eth.abi.encodeParameters(HOTFIX_PARAM_ABI_TYPES, hotfixToParams(proposal, salt))

export const proposalToHash = (kit: ContractKit, proposal: Proposal) =>
keccak256(proposalToEncodedParams(kit, proposal)) as Buffer
export const hotfixToHash = (kit: ContractKit, proposal: Proposal, salt: Buffer) =>
keccak256(hotfixToEncodedParams(kit, proposal, salt)) as Buffer

export interface ProposalTransactionJSON {
contract: CeloContract
Expand Down
4 changes: 4 additions & 0 deletions packages/contractkit/src/utils/web3-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import debugFactory from 'debug'
import Web3 from 'web3'
import { ABIDefinition } from 'web3/eth/abi'
import { CeloProvider } from '../providers/celo-provider'

const debug = debugFactory('kit:web3:utils')
Expand All @@ -18,3 +19,6 @@ export function addLocalAccount(web3: Web3, privateKey: string): Web3 {
debug('Providers configured')
return web3
}

export const getAbiTypes = (abi: ABIDefinition[], methodName: string) =>
abi.find((entry) => entry.name! === methodName)!.inputs!.map((input) => input.type)
37 changes: 19 additions & 18 deletions packages/contractkit/src/wrappers/Governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ export type ProposalParams = Parameters<Governance['methods']['propose']>
export type ProposalTransaction = Pick<Transaction, 'to' | 'input' | 'value'>
export type Proposal = ProposalTransaction[]

export const proposalToParams = (proposal: Proposal): ProposalParams => {
const data = proposal.map((tx) => stringToBuffer(tx.input))
return [
proposal.map((tx) => tx.value),
proposal.map((tx) => tx.to),
bufferToBytes(Buffer.concat(data)),
data.map((inp) => inp.length),
]
}

export interface ProposalRecord {
stage: ProposalStage
metadata: ProposalMetadata
Expand All @@ -80,8 +90,13 @@ export interface Votes {
[VoteValue.Abstain]: BigNumber
}

export type HotfixParams = Parameters<Governance['methods']['executeHotfix']>
export const hotfixToParams = (proposal: Proposal, salt: Buffer): HotfixParams => {
const p = proposalToParams(proposal)
return [p[0], p[1], p[2], p[3], bufferToString(salt)]
}

export interface HotfixRecord {
hash: Buffer
approved: boolean
executed: boolean
preparedEpoch: BigNumber
Expand Down Expand Up @@ -183,16 +198,6 @@ export class GovernanceWrapper extends BaseWrapper<Governance> {
})
)

static toParams = (proposal: Proposal): ProposalParams => {
const data = proposal.map((tx) => stringToBuffer(tx.input))
return [
proposal.map((tx) => tx.value),
proposal.map((tx) => tx.to),
bufferToBytes(Buffer.concat(data)),
data.map((inp) => inp.length),
]
}

/**
* Returns whether a given proposal is approved.
* @param proposalID Governance proposal UUID
Expand Down Expand Up @@ -259,7 +264,7 @@ export class GovernanceWrapper extends BaseWrapper<Governance> {
* Submits a new governance proposal.
* @param proposal Governance proposal
*/
propose = proxySend(this.kit, this.contract.methods.propose, GovernanceWrapper.toParams)
propose = proxySend(this.kit, this.contract.methods.propose, proposalToParams)

/**
* Returns whether a governance proposal exists with the given ID.
Expand Down Expand Up @@ -509,7 +514,6 @@ export class GovernanceWrapper extends BaseWrapper<Governance> {
async getHotfixRecord(hash: Buffer): Promise<HotfixRecord> {
const res = await this.contract.methods.getHotfixRecord(bufferToString(hash)).call()
return {
hash,
approved: res[0],
executed: res[1],
preparedEpoch: valueToBigNumber(res[2]),
Expand Down Expand Up @@ -575,11 +579,8 @@ export class GovernanceWrapper extends BaseWrapper<Governance> {
/**
* Executes a given sequence of transactions if the corresponding hash is prepared and approved.
* @param hotfix Governance hotfix proposal
* @param salt Secret which guarantees uniqueness of hash
* @notice keccak256 hash of abi encoded transactions computed on-chain
*/
executeHotfix = proxySend(
this.kit,
this.contract.methods.executeHotfix,
GovernanceWrapper.toParams
)
executeHotfix = proxySend(this.kit, this.contract.methods.executeHotfix, hotfixToParams)
}
1 change: 1 addition & 0 deletions packages/docs/command-line-interface/governance.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ USAGE
OPTIONS
--from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Executors's address
--jsonTransactions=jsonTransactions (required) Path to json transactions
--salt=salt (required) Secret salt associated with hotfix
```

_See code: [packages/cli/src/commands/governance/executehotfix.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/governance/executehotfix.ts)_
Expand Down
9 changes: 7 additions & 2 deletions packages/protocol/contracts/governance/Governance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,7 @@ contract Governance is
* @param hash The abi encoded keccak256 hash of the hotfix transaction(s) to be whitelisted.
*/
function approveHotfix(bytes32 hash) external {
require(!hotfixes[hash].executed, "hotfix already executed");
Copy link
Contributor

Choose a reason for hiding this comment

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

Make this a modifier?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixing in #2340

require(msg.sender == approver, "Not approver");
hotfixes[hash].approved = true;
emit HotfixApproved(hash);
Expand All @@ -674,6 +675,7 @@ contract Governance is
* @param hash The abi encoded keccak256 hash of the hotfix transaction(s) to be whitelisted.
*/
function whitelistHotfix(bytes32 hash) external {
require(!hotfixes[hash].executed, "hotfix already executed");
hotfixes[hash].whitelisted[msg.sender] = true;
emit HotfixWhitelisted(hash, msg.sender);
}
Expand All @@ -683,6 +685,7 @@ contract Governance is
* @param hash The hash of the hotfix to be prepared.
*/
function prepareHotfix(bytes32 hash) external {
require(!hotfixes[hash].executed, "hotfix already executed");
require(isHotfixPassing(hash), "hotfix not whitelisted by 2f+1 validators");
uint256 epoch = getEpochNumber();
require(hotfixes[hash].preparedEpoch < epoch, "hotfix already prepared for this epoch");
Expand All @@ -696,15 +699,17 @@ contract Governance is
* @param destinations The destination addresses of the proposed transactions.
* @param data The concatenated data to be included in the proposed transactions.
* @param dataLengths The lengths of each transaction's data.
* @param salt Secret associated with hotfix which guarantees uniqueness of hash.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Does this need to be secret?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ditto

* @dev Reverts if hotfix is already executed, not approved, or not prepared for current epoch.
*/
function executeHotfix(
uint256[] calldata values,
address[] calldata destinations,
bytes calldata data,
uint256[] calldata dataLengths
uint256[] calldata dataLengths,
bytes32 salt
) external {
bytes32 hash = keccak256(abi.encode(values, destinations, data, dataLengths));
bytes32 hash = keccak256(abi.encode(values, destinations, data, dataLengths, salt));

(bool approved, bool executed, uint256 preparedEpoch) = getHotfixRecord(hash);
require(!executed, "hotfix already executed");
Expand Down
Loading