Skip to content

Commit 2db0eec

Browse files
committed
feat(solana): governance proposal execution
Signed-off-by: Guilherme Felipe da Silva <[email protected]>
1 parent c6f3b4a commit 2db0eec

File tree

5 files changed

+183
-16
lines changed

5 files changed

+183
-16
lines changed

solana/cli/Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

solana/cli/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ axelar-solana-its = { git = "https://github.com/eigerco/solana-axelar.git", rev
1818
] }
1919
axelar-solana-encoding = { git = "https://github.com/eigerco/solana-axelar.git", rev = "1c97f24" }
2020
axelar-executable = { git = "https://github.com/eigerco/solana-axelar.git", rev = "1c97f24" }
21+
governance-gmp = { git = "https://github.com/eigerco/solana-axelar.git", rev = "1c97f24" }
22+
program-utils = { git = "https://github.com/eigerco/solana-axelar.git", rev = "1c97f24" }
2123

2224
axelar-wasm-std = { git = "https://github.com/eigerco/axelar-amplifier.git", rev = "3f036ec" }
2325
voting-verifier = { git = "https://github.com/eigerco/axelar-amplifier.git", rev = "3f036ec", features = [
@@ -27,8 +29,10 @@ multisig-prover = { git = "https://github.com/eigerco/axelar-amplifier.git", rev
2729
"library",
2830
] }
2931

32+
alloy-sol-types = "0.7.6"
3033
anyhow = "1.0.98"
3134
bincode = { version = "2.0.1", features = ["serde"] }
35+
borsh = "1.5.1"
3236
bs58 = "0.5.1"
3337
clap = { version = "3.2", features = ["derive", "env"] }
3438
cosmrs = { version = "0.16", features = ["cosmwasm", "rpc", "grpc"] }
@@ -54,3 +58,4 @@ walkdir = "2.5.0"
5458
spl-token = { version = "8.0.0", features = ["no-entrypoint"] }
5559
spl-token-2022 = { version = "8.0.1", features = ["no-entrypoint"] }
5660
spl-associated-token-account = { version = "6.0.0", features = ["no-entrypoint"] }
61+
base64 = "0.22.1"

solana/cli/src/governance.rs

Lines changed: 137 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
1-
use clap::{Parser, Subcommand};
2-
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
1+
use axelar_solana_governance::instructions::builder::IxBuilder;
2+
use base64::Engine;
3+
use clap::{Args, Subcommand};
4+
use solana_sdk::{instruction::AccountMeta, instruction::Instruction, pubkey::Pubkey};
35

46
use crate::{
57
config::Config,
68
types::ChainNameOnAxelar,
79
utils::{
8-
read_json_file_from_path, write_json_to_file_path, ADDRESS_KEY, CHAINS_KEY,
9-
CONFIG_ACCOUNT_KEY, CONTRACTS_KEY, GOVERNANCE_ADDRESS_KEY, GOVERNANCE_CHAIN_KEY,
10-
GOVERNANCE_KEY, UPGRADE_AUTHORITY_KEY,
10+
parse_account_meta_string, read_json_file_from_path, write_json_to_file_path, ADDRESS_KEY,
11+
CHAINS_KEY, CONFIG_ACCOUNT_KEY, CONTRACTS_KEY, GOVERNANCE_ADDRESS_KEY,
12+
GOVERNANCE_CHAIN_KEY, GOVERNANCE_KEY, MINIMUM_PROPOSAL_ETA_DELAY_KEY,
13+
UPGRADE_AUTHORITY_KEY,
1114
},
1215
};
1316

1417
#[derive(Subcommand, Debug)]
1518
pub(crate) enum Commands {
16-
#[clap(long_about = "Initialize the Gateway program")]
19+
#[clap(long_about = "Initialize the Governance program")]
1720
Init(InitArgs),
21+
22+
#[clap(long_about = "Execute a scheduled proposal after its ETA")]
23+
ExecuteProposal(ExecuteProposalArgs),
24+
25+
#[clap(long_about = "Execute an operator-approved proposal (bypasses ETA)")]
26+
ExecuteOperatorProposal(ExecuteOperatorProposalArgs),
1827
}
1928

20-
#[derive(Parser, Debug)]
29+
#[derive(Args, Debug)]
2130
pub(crate) struct InitArgs {
2231
#[clap(short, long)]
2332
governance_chain: String,
@@ -32,24 +41,86 @@ pub(crate) struct InitArgs {
3241
operator: Pubkey,
3342
}
3443

44+
// Common arguments for proposal execution
45+
#[derive(Args, Debug, Clone)]
46+
struct ProposalExecutionBaseArgs {
47+
#[clap(long, help = "Target program ID for the proposal's instruction")]
48+
target: Pubkey,
49+
50+
#[clap(
51+
long,
52+
help = "Amount of native value (lamports) to transfer with the proposal"
53+
)]
54+
native_value: u64,
55+
56+
#[clap(
57+
long,
58+
help = "Base64 encoded call data for the target program instruction"
59+
)]
60+
calldata: String,
61+
62+
#[clap(
63+
long,
64+
help = "Account metas required by the target program instruction. Format: 'pubkey:is_signer:is_writable'",
65+
value_parser = parse_account_meta_string,
66+
)]
67+
target_accounts: Vec<AccountMeta>,
68+
69+
#[clap(long, help = "Optional account to receive the native value transfer")]
70+
native_value_receiver: Option<Pubkey>,
71+
}
72+
73+
#[derive(Args, Debug)]
74+
pub(crate) struct ExecuteProposalArgs {
75+
#[clap(flatten)]
76+
base: ProposalExecutionBaseArgs,
77+
}
78+
79+
#[derive(Args, Debug)]
80+
pub(crate) struct ExecuteOperatorProposalArgs {
81+
#[clap(flatten)]
82+
base: ProposalExecutionBaseArgs,
83+
84+
#[clap(long, help = "Operator pubkey (must be a signer of the transaction)")]
85+
operator: Pubkey,
86+
}
87+
88+
#[derive(Args, Debug)]
89+
pub(crate) struct TransferOperatorshipArgs {
90+
#[clap(long, help = "Pubkey of the new operator")]
91+
new_operator: Pubkey,
92+
93+
#[clap(
94+
long,
95+
help = "Pubkey of the current operator (must be a signer of the transaction)"
96+
)]
97+
operator: Pubkey,
98+
}
99+
35100
pub(crate) async fn build_instruction(
36101
fee_payer: &Pubkey,
37102
command: Commands,
38103
config: &Config,
39104
) -> eyre::Result<Instruction> {
105+
let (config_pda, _) = axelar_solana_governance::state::GovernanceConfig::pda();
106+
40107
match command {
41-
Commands::Init(init_args) => init(fee_payer, init_args, config).await,
108+
Commands::Init(init_args) => init(fee_payer, init_args, config, &config_pda).await,
109+
Commands::ExecuteProposal(args) => execute_proposal(fee_payer, args, &config_pda).await,
110+
Commands::ExecuteOperatorProposal(args) => {
111+
execute_operator_proposal(fee_payer, args, &config_pda).await
112+
}
42113
}
43114
}
44115

45116
async fn init(
46117
fee_payer: &Pubkey,
47118
init_args: InitArgs,
48119
config: &Config,
120+
config_pda: &Pubkey,
49121
) -> eyre::Result<Instruction> {
50122
let chain_hash = solana_sdk::keccak::hashv(&[init_args.governance_chain.as_bytes()]).0;
51123
let address_hash = solana_sdk::keccak::hashv(&[init_args.governance_address.as_bytes()]).0;
52-
let (config_pda, _bump) = axelar_solana_governance::state::GovernanceConfig::pda();
53124

54125
let governance_config = axelar_solana_governance::state::GovernanceConfig::new(
55126
chain_hash,
@@ -66,13 +137,65 @@ async fn init(
66137
UPGRADE_AUTHORITY_KEY: fee_payer.to_string(),
67138
GOVERNANCE_ADDRESS_KEY: init_args.governance_address,
68139
GOVERNANCE_CHAIN_KEY: init_args.governance_chain,
140+
MINIMUM_PROPOSAL_ETA_DELAY_KEY: init_args.minimum_proposal_eta_delay,
69141
});
70142

71143
write_json_to_file_path(&chains_info, &config.chains_info_file)?;
72144

73-
Ok(
74-
axelar_solana_governance::instructions::builder::IxBuilder::new()
75-
.initialize_config(fee_payer, &config_pda, governance_config)
76-
.build(),
77-
)
145+
Ok(IxBuilder::new()
146+
.initialize_config(fee_payer, config_pda, governance_config)
147+
.build())
148+
}
149+
150+
async fn execute_proposal(
151+
fee_payer: &Pubkey,
152+
args: ExecuteProposalArgs,
153+
config_pda: &Pubkey,
154+
) -> eyre::Result<Instruction> {
155+
let calldata_bytes = base64::engine::general_purpose::STANDARD.decode(args.base.calldata)?;
156+
let native_value_receiver_account = args
157+
.base
158+
.native_value_receiver
159+
.map(|pk| AccountMeta::new(pk, false));
160+
161+
// Note: ETA is part of the proposal data stored on-chain, not provided here.
162+
// The builder calculates the proposal hash based on target, calldata, native_value.
163+
// The ETA value used in `with_proposal_data` is only relevant for *scheduling*,
164+
// not execution, but the builder requires some value. We use 0 here.
165+
let builder = IxBuilder::new().with_proposal_data(
166+
args.base.target,
167+
args.base.native_value,
168+
0,
169+
native_value_receiver_account,
170+
&args.base.target_accounts,
171+
calldata_bytes,
172+
);
173+
174+
Ok(builder.execute_proposal(fee_payer, config_pda).build())
175+
}
176+
177+
async fn execute_operator_proposal(
178+
fee_payer: &Pubkey,
179+
args: ExecuteOperatorProposalArgs,
180+
config_pda: &Pubkey,
181+
) -> eyre::Result<Instruction> {
182+
let calldata_bytes = base64::engine::general_purpose::STANDARD.decode(args.base.calldata)?;
183+
let native_value_receiver_account = args
184+
.base
185+
.native_value_receiver
186+
.map(|pk| AccountMeta::new(pk, false));
187+
188+
// ETA is irrelevant for operator execution. Use 0.
189+
let builder = IxBuilder::new().with_proposal_data(
190+
args.base.target,
191+
args.base.native_value,
192+
0,
193+
native_value_receiver_account,
194+
&args.base.target_accounts,
195+
calldata_bytes,
196+
);
197+
198+
Ok(builder
199+
.execute_operator_proposal(fee_payer, config_pda, &args.operator)
200+
.build())
78201
}

solana/cli/src/main.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ async fn run() -> eyre::Result<()> {
212212

213213
let config = Config::new(cli.url, cli.output_dir, cli.chains_info_dir)?;
214214

215+
// Proceed with building and potentially sending/signing/broadcasting a Solana transaction
215216
match cli.command {
216217
Command::Send(args) => {
217218
let send_args = SendArgs {
@@ -276,8 +277,8 @@ async fn build_instruction(
276277
InstructionSubcommand::Its(command) => {
277278
its::build_instruction(fee_payer, command, config).await?
278279
}
279-
InstructionSubcommand::Governance(commands) => {
280-
governance::build_instruction(fee_payer, commands, config).await?
280+
InstructionSubcommand::Governance(command) => {
281+
governance::build_instruction(fee_payer, command, config).await?
281282
}
282283
};
283284

solana/cli/src/utils.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::error::{AppError, Result};
1111
use crate::types::{
1212
NetworkType, PartialSignature, SignedSolanaTransaction, UnsignedSolanaTransaction,
1313
};
14+
pub(crate) use solana_sdk::instruction::AccountMeta;
1415

1516
pub(crate) const ADDRESS_KEY: &str = "address";
1617
pub(crate) const AXELAR_KEY: &str = "axelar";
@@ -148,6 +149,38 @@ pub(crate) fn encode_its_destination(
148149
}
149150
}
150151

152+
/// Parses a string representation of an AccountMeta.
153+
/// Format: "pubkey:is_signer:is_writable" (e.g., "SomePubkey...:false:true")
154+
pub fn parse_account_meta_string(s: &str) -> Result<AccountMeta> {
155+
let parts: Vec<&str> = s.split(':').collect();
156+
if parts.len() != 3 {
157+
return Err(AppError::InvalidInput(format!(
158+
"Invalid AccountMeta format: '{}'. Expected 'pubkey:is_signer:is_writable'",
159+
s
160+
)));
161+
}
162+
163+
let pubkey = Pubkey::from_str(parts[0])?;
164+
let is_signer = bool::from_str(parts[1]).map_err(|_| {
165+
AppError::InvalidInput(format!(
166+
"Invalid is_signer value: '{}'. Expected 'true' or 'false'",
167+
parts[1]
168+
))
169+
})?;
170+
let is_writable = bool::from_str(parts[2]).map_err(|_| {
171+
AppError::InvalidInput(format!(
172+
"Invalid is_writable value: '{}'. Expected 'true' or 'false'",
173+
parts[2]
174+
))
175+
})?;
176+
177+
Ok(if is_writable {
178+
AccountMeta::new(pubkey, is_signer)
179+
} else {
180+
AccountMeta::new_readonly(pubkey, is_signer)
181+
})
182+
}
183+
151184
pub(crate) fn print_transaction_result(config: &Config, result: Result<Signature>) -> Result<()> {
152185
match result {
153186
Ok(tx_signature) => {

0 commit comments

Comments
 (0)