Skip to content
Open
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
15 changes: 8 additions & 7 deletions evm/cli-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,22 +85,23 @@ interface GovernanceOptions {
const addGovernanceOptions = (program: Command): Command => {
program.addOption(new Option('--governance', 'Submit this change via interchain governance'));
program.addOption(
new Option(
'--governanceContract <governanceContract>',
'Governance contract to target (InterchainGovernance or AxelarServiceGovernance)',
)
new Option('--governanceContract <governanceContract>', 'Governance contract to target')
.choices(['InterchainGovernance', 'AxelarServiceGovernance'])
.default('AxelarServiceGovernance'),
);
program.addOption(new Option('--operatorProposal', 'Generate an operator-style proposal (ApproveOperator)').default(false));
program.addOption(new Option('--operatorProposal', 'Generate an operator-style proposal').default(false));
program.addOption(
new Option(
'--activationTime <activationTime>',
'Governance Activation Time (YYYY-MM-DDTHH:mm:ss UTC) or 0 for immediate scheduling (subject to min timelock)',
'Governance Activation Time (format: YYYY-MM-DDTHH:mm:ss UTC) or 0 for immediate scheduling (subject to min timelock)',
).default('0'),
);
program.addOption(new Option('--generate-only <file>', 'Generate Axelar proposal JSON to the given file instead of submitting'));
program.addOption(new Option('--generate-only <file>', 'Generate Axelar proposal JSON to the given file with path'));
program.addOption(
new Option('--standardProposal', 'submit as a standard proposal instead of expedited (default is expedited)').default(false),
);
program.addOption(new Option('-m, --mnemonic <mnemonic>', 'mnemonic').env('MNEMONIC'));
program.addOption(new Option('--proposal-type <type>', 'proposal type').choices(['create', 'cancel']).default('create'));

return program;
};
Expand Down
6 changes: 4 additions & 2 deletions evm/docs/contract-ownership.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,14 @@ ts-node evm/ownership.js --governance --operatorProposal \
- `--governanceContract <InterchainGovernance|AxelarServiceGovernance>`: Governance contract to target (default: `AxelarServiceGovernance`)
- `--operatorProposal`: Generate operator-style proposal (bypasses timelock; requires `AxelarServiceGovernance`)
- `--activationTime <time>`: ETA as `YYYY-MM-DDTHH:mm:ss` UTC or `0`
- `--generate-only <file>`: Write proposal JSON to file instead of submitting
- `--generate-only <file>`: Write proposal JSON to file instead of submitting (provide a path)
- `--standardProposal`: Submit as standard proposal instead of expedited (default is expedited)
- `--proposal-type <create|cancel>`: Explicitly set proposal type (default: `create`)
- `--mnemonic`: Mnemonic for submitting to Axelar (uses `MNEMONIC` environment variable if set)

## Notes

- In proposal mode, calldata is generated via the ABI using `populateTransaction` for safety.
- Multi-chain execution is supported via `-n chainA,chainB` (use `--parallel` for concurrency).
- If you use `--generate-only` with multiple chains, use a unique filename per chain (otherwise parallel runs can overwrite the same file).
- If `--operatorProposal` is set with `InterchainGovernance`, the command will fail because operator approvals require `AxelarServiceGovernance`.
- Use `--address <address>` to override the contract address from chain configuration.
88 changes: 88 additions & 0 deletions evm/docs/governance-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ governance proposals directly by passing `--governance`. These scripts share a c
- Treats the generated proposal as an **operator-based proposal** (uses `ApproveOperator` under the hood).
- Only valid when `--governanceContract AxelarServiceGovernance` is used.
- If omitted, a standard timelock proposal (`ScheduleTimelock`) is generated instead.
- **`--proposal-type <create|cancel>`**:
- `create` (default): create a new timelock / operator proposal
- `cancel`: create a cancellation for an existing scheduled proposal
- **Important**: cancellation must use the **same inputs** as the original proposal (target, calldata, nativeValue, and activation time / ETA parameters that affect the payload).

**Example (gateway – timelock style via InterchainGovernance):**

Expand Down Expand Up @@ -225,6 +229,90 @@ ts-node evm/gateway.js \
--activationTime <activationTime>
```

### Cancelling proposals created by helper scripts (Gateway / ITS / Ownership)

To cancel a previously scheduled helper‑script proposal, rerun the **same** command with:

- `--proposal-type cancel`
- the **same** `--activationTime`
- the same action arguments (e.g. `--destination`, `--newOperator`, token id, etc.)
- the same governance mode flags (`--governanceContract`, `--operatorProposal`, and `--nativeValue` if used)

#### Cancel a Gateway transferGovernance proposal

```bash
ts-node evm/gateway.js \
--action transferGovernance \
--destination 0xNewGovernorAddress \
--governance \
--activationTime <activationTime> \
--proposal-type cancel
```

#### Cancel a Gateway transferOperatorship proposal (Amplifier)

```bash
ts-node evm/gateway.js \
--action transferOperatorship \
--newOperator 0xNewOperatorAddress \
--governance \
--activationTime <activationTime> \
--proposal-type cancel
```

#### Cancel ITS pause/unpause or migrate proposals

```bash
# Cancel Pause ITS
ts-node evm/its.js set-pause-status true \
--governance \
--activationTime <activationTime> \
--proposal-type cancel

# Cancel Unpause ITS
ts-node evm/its.js set-pause-status false \
--governance \
--activationTime <activationTime> \
--proposal-type cancel

# Cancel migrate interchain token
ts-node evm/its.js migrate-interchain-token 0x0000...0000 \
--governance \
--activationTime <activationTime> \
--proposal-type cancel
```

#### Cancel ITS trusted chains changes (multicall-based)

```bash
# Cancel set trusted chains
ts-node evm/its.js set-trusted-chains ethereum avalanche \
--governance \
--activationTime <activationTime> \
--proposal-type cancel

# Cancel remove trusted chains
ts-node evm/its.js remove-trusted-chains ethereum avalanche \
--governance \
--activationTime <activationTime> \
--proposal-type cancel
```

#### Ownership proposals (IOwnable)

```bash
# Create transferOwnership proposal
ts-node evm/ownership.js --governance \
-n <chain> -c AxelarGateway --action transferOwnership --newOwner 0xNewOwnerAddress \
--activationTime <activationTime>

# Cancel transferOwnership proposal
ts-node evm/ownership.js --governance \
-n <chain> -c AxelarGateway --action transferOwnership --newOwner 0xNewOwnerAddress \
--activationTime <activationTime> \
--proposal-type cancel
```

### InterchainTokenService Governance Actions

#### Set Trusted Chains via ITS
Expand Down
138 changes: 41 additions & 97 deletions evm/gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,17 @@ const {
getEVMAddresses,
isValidAddress,
validateParameters,
wasEventEmitted,
mainProcessor,
wasEventEmitted,
printError,
getGasOptions,
httpGet,
getContractJSON,
getMultisigProof,
getGovernanceContract,
createGovernanceProposal,
writeJSON,
executeDirectlyOrSubmitProposal,
} = require('./utils');
const { addBaseOptions, addGovernanceOptions } = require('./cli-utils');
const { getWallet, signTransaction } = require('./sign-utils');
const { ProposalType, encodeGovernanceProposal, submitProposalToAxelar } = require('./governance');
const { createGMPProposalJSON, dateToEta } = require('../common/utils');

const AxelarGateway = require('@axelar-network/axelar-cgp-solidity/artifacts/contracts/AxelarGateway.sol/AxelarGateway.json');
const IAxelarAmplifierGateway = require('@axelar-network/axelar-gmp-sdk-solidity/interfaces/IAxelarAmplifierGateway.json');
Expand Down Expand Up @@ -380,41 +376,36 @@ async function processCommand(axelar, chain, _chains, options) {
const currGovernance = await gateway.governance();
Comment thread
cursor[bot] marked this conversation as resolved.
printInfo('Current governance', currGovernance);

if (options.governance) {
const { data: calldata } = await gateway.populateTransaction.transferGovernance(newGovernance, gasOptions);
if (!options.governance) {
const isCurrentGovernor = currGovernance.toLowerCase() === walletAddress.toLowerCase();

return createGovernanceProposal({
chain,
options,
targetAddress: gatewayAddress,
calldata,
ProposalType,
encodeGovernanceProposal,
createGMPProposalJSON,
dateToEta,
});
if (!isCurrentGovernor) {
throw new Error(`Caller ${walletAddress} is not the current governor (${currGovernance})`);
}
}

if (!(currGovernance === walletAddress)) {
throw new Error('Wallet address is not the governor');
}
const governanceProposalSubmitted = await executeDirectlyOrSubmitProposal(
chain,
gateway,
'transferGovernance',
[newGovernance],
options,
'0',
['GovernanceTransferred'],
);

if (prompt(`Proceed with governance transfer to ${chalk.cyan(newGovernance)}`, yes)) {
return;
if (governanceProposalSubmitted) {
break;
}

const tx = await gateway.transferGovernance(newGovernance, gasOptions);
printInfo('Transfer governance tx', tx.hash);

const receipt = await tx.wait(chain.confirmations);

const eventEmitted = wasEventEmitted(receipt, gateway, 'GovernanceTransferred');
const updatedGovernance = await gateway.governance();
printInfo('New governance', updatedGovernance);

if (!eventEmitted) {
throw new Error('Event not emitted in receipt.');
if (updatedGovernance.toLowerCase() !== newGovernance.toLowerCase()) {
throw new Error('Governance transfer failed.');
}

chain.contracts.AxelarGateway.governance = newGovernance;
chain.contracts.AxelarGateway.governance = updatedGovernance;

break;
}
Expand Down Expand Up @@ -537,41 +528,28 @@ async function processCommand(axelar, chain, _chains, options) {
printInfo('Current operator', currOperator);

const owner = await gateway.owner();
const isCurrentOperator = currOperator.toLowerCase() === walletAddress.toLowerCase();
const isOwner = owner.toLowerCase() === walletAddress.toLowerCase();

if (options.governance) {
const { data: calldata } = await gateway.populateTransaction.transferOperatorship(newOperator, gasOptions);
if (!options.governance) {
const isCurrentOperator = currOperator.toLowerCase() === walletAddress.toLowerCase();
const isOwner = owner.toLowerCase() === walletAddress.toLowerCase();

return createGovernanceProposal({
chain,
options,
targetAddress: gatewayAddress,
calldata,
ProposalType,
encodeGovernanceProposal,
createGMPProposalJSON,
dateToEta,
});
if (!isCurrentOperator && !isOwner) {
throw new Error(`Caller ${walletAddress} is neither the current operator (${currOperator}) nor the owner (${owner})`);
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

if (!isCurrentOperator && !isOwner) {
throw new Error(`Caller ${walletAddress} is neither the current operator (${currOperator}) nor the owner (${owner})`);
}

if (prompt(`Proceed with operatorship transfer to ${chalk.cyan(newOperator)}`, yes)) {
return;
}

const tx = await gateway.transferOperatorship(newOperator, gasOptions);
printInfo('Transfer operatorship tx', tx.hash);

const receipt = await tx.wait(chain.confirmations);

const eventEmitted = wasEventEmitted(receipt, gateway, 'OperatorshipTransferred');
const governanceProposalSubmitted = await executeDirectlyOrSubmitProposal(
chain,
gateway,
'transferOperatorship',
[newOperator],
options,
'0',
['OperatorshipTransferred'],
);

if (!eventEmitted) {
throw new Error('Event not emitted in receipt.');
if (governanceProposalSubmitted) {
break;
}

const updatedOperator = await gateway.operator();
Expand Down Expand Up @@ -647,41 +625,7 @@ async function processCommand(axelar, chain, _chains, options) {
}

async function main(options) {
if (!options.governance) {
await mainProcessor(options, processCommand);
return;
}

const proposals = [];

await mainProcessor(options, (axelar, chain, chains, opts) =>
processCommand(axelar, chain, chains, opts).then((proposal) => {
if (proposal) {
proposals.push(proposal);
}
}),
);

if (proposals.length > 0) {
const proposal = {
title: 'Gateway Governance Proposal',
description: 'Gateway Governance Proposal',
contract_calls: proposals,
};

const proposalJSON = JSON.stringify(proposal, null, 2);

printInfo('Proposal', proposalJSON);

if (options.generateOnly) {
writeJSON(proposal, options.generateOnly);
printInfo('Proposal written to file', options.generateOnly);
} else {
if (!prompt('Proceed with submitting this proposal to Axelar?', options.yes)) {
await submitProposalToAxelar(proposal, options);
}
}
}
await mainProcessor(options, processCommand);
}

if (require.main === module) {
Expand Down
Loading