diff --git a/.changelog/unreleased/features/4402-override-rewards-target.md b/.changelog/unreleased/features/4402-override-rewards-target.md new file mode 100644 index 0000000000..33ccd8f095 --- /dev/null +++ b/.changelog/unreleased/features/4402-override-rewards-target.md @@ -0,0 +1,2 @@ +- Allow to override the receiver target for PoS claim rewards transaction. + ([\#4402](https://github.com/anoma/namada/pull/4402)) \ No newline at end of file diff --git a/.changelog/unreleased/features/4427-sdk-override-rewards-target.md b/.changelog/unreleased/features/4427-sdk-override-rewards-target.md new file mode 100644 index 0000000000..ac9d8f9234 --- /dev/null +++ b/.changelog/unreleased/features/4427-sdk-override-rewards-target.md @@ -0,0 +1,2 @@ +- SDK and client and now optionally override the receiver target for PoS claim + rewards transaction. ([\#4427](https://github.com/anoma/namada/pull/4427)) \ No newline at end of file diff --git a/crates/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index e9d3e40ac4..00bda01d9c 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -3627,6 +3627,7 @@ pub mod args { pub const RAW_PUBLIC_KEY_HASH_OPT: ArgOpt = RAW_PUBLIC_KEY_HASH.opt(); pub const RECEIVER: Arg = arg("receiver"); + pub const RECEIVER_ADDR: ArgOpt = arg_opt("receiver"); pub const REFUND_TARGET: ArgOpt = arg_opt("refund-target"); pub const RELAYER: Arg
= arg("relayer"); @@ -6359,6 +6360,7 @@ pub mod args { tx, validator: chain_ctx.get(&self.validator), source: self.source.map(|x| chain_ctx.get(&x)), + receiver: self.receiver.map(|x| chain_ctx.get(&x)), tx_code_path: self.tx_code_path.to_path_buf(), }) } @@ -6369,11 +6371,13 @@ pub mod args { let tx = Tx::parse(matches); let validator = VALIDATOR.parse(matches); let source = SOURCE_OPT.parse(matches); + let receiver = RECEIVER_ADDR.parse(matches); let tx_code_path = PathBuf::from(TX_CLAIM_REWARDS_WASM); Self { tx, validator, source, + receiver, tx_code_path, } } @@ -6385,6 +6389,11 @@ pub mod args { "Source address for claiming rewards for a bond. For \ self-bonds, the validator is also the source." ))) + .arg(RECEIVER_ADDR.def().help(wrap!( + "An optional receiver address. If not given, the rewards \ + will be received by the owner (i.e. the source of a \ + delegation or the validator of a self-bond)." + ))) } } diff --git a/crates/node/src/shell/finalize_block.rs b/crates/node/src/shell/finalize_block.rs index d54defe8d7..e61f479980 100644 --- a/crates/node/src/shell/finalize_block.rs +++ b/crates/node/src/shell/finalize_block.rs @@ -2444,15 +2444,18 @@ mod test_finalize_block { .unwrap(); // Claim the rewards from the initial epoch - let reward_1 = - proof_of_stake::claim_reward_tokens::< - _, - governance::Store<_>, - token::Store<_>, - >( - &mut shell.state, None, &validator.address, current_epoch - ) - .unwrap(); + let reward_1 = proof_of_stake::claim_reward_tokens::< + _, + governance::Store<_>, + token::Store<_>, + >( + &mut shell.state, + None, + &validator.address, + None, + current_epoch, + ) + .unwrap(); total_claimed += reward_1; assert_eq!(reward_1, query_rewards); assert!(is_reward_equal_enough(total_rewards, total_claimed, 1)); @@ -2481,7 +2484,11 @@ mod test_finalize_block { governance::Store<_>, token::Store<_>, >( - &mut shell.state, None, &validator.address, current_epoch + &mut shell.state, + None, + &validator.address, + None, + current_epoch, ) .unwrap(); assert_eq!(att, token::Amount::zero()); @@ -2520,7 +2527,11 @@ mod test_finalize_block { governance::Store<_>, token::Store<_>, >( - &mut shell.state, None, &validator.address, current_epoch + &mut shell.state, + None, + &validator.address, + None, + current_epoch, ) .unwrap(); total_claimed += rew; @@ -2601,15 +2612,18 @@ mod test_finalize_block { .unwrap(); // Claim tokens - let reward_2 = - proof_of_stake::claim_reward_tokens::< - _, - governance::Store<_>, - token::Store<_>, - >( - &mut shell.state, None, &validator.address, current_epoch - ) - .unwrap(); + let reward_2 = proof_of_stake::claim_reward_tokens::< + _, + governance::Store<_>, + token::Store<_>, + >( + &mut shell.state, + None, + &validator.address, + None, + current_epoch, + ) + .unwrap(); total_claimed += reward_2; assert_eq!(query_rewards, reward_2); @@ -2730,15 +2744,18 @@ mod test_finalize_block { } // Claim the rewards for the validator for the first two epochs - let val_reward_1 = - proof_of_stake::claim_reward_tokens::< - _, - governance::Store<_>, - token::Store<_>, - >( - &mut shell.state, None, &validator.address, current_epoch - ) - .unwrap(); + let val_reward_1 = proof_of_stake::claim_reward_tokens::< + _, + governance::Store<_>, + token::Store<_>, + >( + &mut shell.state, + None, + &validator.address, + None, + current_epoch, + ) + .unwrap(); total_claimed += val_reward_1; assert!(is_reward_equal_enough( total_rewards, @@ -2758,15 +2775,18 @@ mod test_finalize_block { total_rewards += inflation_3; // Claim again for the validator - let val_reward_2 = - proof_of_stake::claim_reward_tokens::< - _, - governance::Store<_>, - token::Store<_>, - >( - &mut shell.state, None, &validator.address, current_epoch - ) - .unwrap(); + let val_reward_2 = proof_of_stake::claim_reward_tokens::< + _, + governance::Store<_>, + token::Store<_>, + >( + &mut shell.state, + None, + &validator.address, + None, + current_epoch, + ) + .unwrap(); // Claim for the delegator let del_reward_1 = proof_of_stake::claim_reward_tokens::< @@ -2777,6 +2797,7 @@ mod test_finalize_block { &mut shell.state, Some(&delegator), &validator.address, + None, current_epoch, ) .unwrap(); diff --git a/crates/proof_of_stake/src/lib.rs b/crates/proof_of_stake/src/lib.rs index 7213203bab..78ade9ef0d 100644 --- a/crates/proof_of_stake/src/lib.rs +++ b/crates/proof_of_stake/src/lib.rs @@ -2805,6 +2805,7 @@ pub fn claim_reward_tokens( storage: &mut S, source: Option<&Address>, validator: &Address, + receiver: Option<&Address>, current_epoch: Epoch, ) -> Result where @@ -2814,8 +2815,14 @@ where { tracing::debug!("Claiming rewards in epoch {current_epoch}"); - let source = source.cloned().unwrap_or_else(|| validator.clone()); - tracing::debug!("Source {} --> Validator {}", source, validator); + let source = source.unwrap_or(validator).clone(); + let receiver = receiver.unwrap_or(&source).clone(); + tracing::debug!( + "Source {} --> Validator {}, Receiver {}", + source, + validator, + receiver + ); let mut reward_tokens = compute_current_rewards_from_bonds::( storage, @@ -2832,9 +2839,15 @@ where // Update the last claim epoch in storage write_last_reward_claim_epoch(storage, &source, validator, current_epoch)?; - // Transfer the bonded tokens from PoS to the source + // Transfer the bonded tokens from PoS to the receiver let staking_token = staking_token_address(storage); - Token::transfer(storage, &staking_token, &ADDRESS, &source, reward_tokens)?; + Token::transfer( + storage, + &staking_token, + &ADDRESS, + &receiver, + reward_tokens, + )?; Token::emit_transfer_event( storage, CLAIM_REWARDS_EVENT_DESC.into(), @@ -2842,7 +2855,7 @@ where &staking_token, reward_tokens, trans_token::UserAccount::Internal(ADDRESS), - trans_token::UserAccount::Internal(source), + trans_token::UserAccount::Internal(receiver), )?; Ok(reward_tokens) diff --git a/crates/proof_of_stake/src/vp.rs b/crates/proof_of_stake/src/vp.rs index f6944b528e..d87b914911 100644 --- a/crates/proof_of_stake/src/vp.rs +++ b/crates/proof_of_stake/src/vp.rs @@ -105,7 +105,9 @@ where // The key is src bond ID and value is pair of (dest_validator, amount) let mut redelegations: BTreeMap = Default::default(); - let mut claimed_rewards: BTreeSet = Default::default(); + // The value is an optional rewards receiver + let mut claimed_rewards: BTreeMap> = + Default::default(); let mut changed_commission: BTreeSet
= Default::default(); let mut changed_metadata: BTreeSet
= Default::default(); let mut changed_consensus_key: BTreeSet
= Default::default(); @@ -241,6 +243,7 @@ where PosAction::ClaimRewards(ClaimRewards { validator, source, + receiver, }) => { let bond_id = BondId { source: source.unwrap_or_else(|| validator.clone()), @@ -256,7 +259,7 @@ where ) .into()); } - claimed_rewards.insert(bond_id); + claimed_rewards.insert(bond_id, receiver); } PosAction::CommissionChange(validator) => { if !verifiers.contains(&validator) { diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index 233bbfd976..daf4cefeef 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -1880,6 +1880,10 @@ pub struct ClaimRewards { /// Source address for claiming rewards due to bonds. For self-bonds, the /// validator is also the source pub source: Option, + /// An optional receiver address. If not given, the rewards will be + /// received by the owner (i.e. the source of a delegation or the + /// validator of a self-bond). + pub receiver: Option, /// Path to the TX WASM code file pub tx_code_path: PathBuf, } diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 76579ce7e8..0be87fdbde 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -535,6 +535,7 @@ pub trait Namada: NamadaIo { args::ClaimRewards { validator, source: None, + receiver: None, tx_code_path: PathBuf::from(TX_CLAIM_REWARDS_WASM), tx: self.tx_builder(), } diff --git a/crates/sdk/src/queries/vp/pos.rs b/crates/sdk/src/queries/vp/pos.rs index 71f1e6542c..5abe1e5ce1 100644 --- a/crates/sdk/src/queries/vp/pos.rs +++ b/crates/sdk/src/queries/vp/pos.rs @@ -1248,14 +1248,15 @@ mod test { .expect("Test failed"); client.state.in_mem_mut().block.height = height + 1; - let claimed = namada_proof_of_stake::claim_reward_tokens::< - _, - governance::Store<_>, - namada_token::Store<_>, - >( - &mut client.state, Some(&delegator), &validator, epoch - ) - .expect("Claiming rewards failed"); + let claimed = + namada_proof_of_stake::claim_reward_tokens::< + _, + governance::Store<_>, + namada_token::Store<_>, + >( + &mut client.state, Some(&delegator), &validator, None, epoch + ) + .expect("Claiming rewards failed"); assert_eq!(claimed, del_reward_epoch_3 + del_reward_epoch_2); @@ -1264,7 +1265,7 @@ mod test { _, governance::Store<_>, namada_token::Store<_>, - >(&mut client.state, None, &validator, epoch) + >(&mut client.state, None, &validator, None, epoch) .expect("Claiming validator rewards failed"); assert_eq!(claimed_validator, val_reward_epoch_3 + val_reward_epoch_2); diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index 31237d76a0..3dfe27acce 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -1726,6 +1726,7 @@ pub async fn build_claim_rewards( tx: tx_args, validator, source, + receiver, tx_code_path, }: &args::ClaimRewards, ) -> Result<(Tx, SigningTxData)> { @@ -1757,7 +1758,30 @@ pub async fn build_claim_rewards( None => Ok(source.clone()), }?; - let data = pos::ClaimRewards { validator, source }; + // Check that the receiver address exists on chain + let receiver = match receiver.clone() { + Some(receiver) => { + let message = format!( + "The receiver address {receiver} doesn't exist on chain." + ); + address_exists_or_err( + receiver, + tx_args.force, + context, + message, + |err| Error::from(TxSubmitError::LocationDoesNotExist(err)), + ) + .await + .map(Some) + } + None => Ok(receiver.clone()), + }?; + + let data = pos::ClaimRewards { + validator, + source, + receiver, + }; build( context, diff --git a/crates/trans_token/src/vp.rs b/crates/trans_token/src/vp.rs index 8675f015ed..6b943dc9d2 100644 --- a/crates/trans_token/src/vp.rs +++ b/crates/trans_token/src/vp.rs @@ -297,7 +297,11 @@ where fn has_bal_inc_protocol_action(action: &Action, owner: Owner<'_>) -> bool { match action { Action::Pos( - PosAction::ClaimRewards(ClaimRewards { validator, source }) + PosAction::ClaimRewards(ClaimRewards { + validator, + source, + receiver: _, + }) | PosAction::Withdraw(Withdraw { validator, source }), ) => match owner { Owner::Account(owner) => { @@ -1099,6 +1103,7 @@ mod tests { .push_action(Action::Pos(PosAction::ClaimRewards(ClaimRewards { validator: established_address_1(), source: None, + receiver: None, }))) .unwrap(); diff --git a/crates/tx/src/data/pos.rs b/crates/tx/src/data/pos.rs index a2047316c3..4774d936a6 100644 --- a/crates/tx/src/data/pos.rs +++ b/crates/tx/src/data/pos.rs @@ -128,6 +128,8 @@ pub struct ClaimRewards { /// Source address for claiming rewards from a bond. For self-bonds, the /// validator is also the source pub source: Option
, + /// Optional rewards receiver address + pub receiver: Option
, } /// A redelegation of bonded tokens from one validator to another. diff --git a/crates/tx_prelude/src/proof_of_stake.rs b/crates/tx_prelude/src/proof_of_stake.rs index a920b5ec2d..5efc61de3a 100644 --- a/crates/tx_prelude/src/proof_of_stake.rs +++ b/crates/tx_prelude/src/proof_of_stake.rs @@ -203,6 +203,7 @@ impl Ctx { &mut self, source: Option<&Address>, validator: &Address, + receiver: Option<&Address>, ) -> Result { // The tx must be authorized by the source address let verifier = source.as_ref().unwrap_or(&validator); @@ -211,6 +212,7 @@ impl Ctx { self.push_action(Action::Pos(PosAction::ClaimRewards(ClaimRewards { validator: validator.clone(), source: source.cloned(), + receiver: receiver.cloned(), })))?; let current_epoch = self.get_block_epoch()?; @@ -218,6 +220,7 @@ impl Ctx { self, source, validator, + receiver, current_epoch, ) } diff --git a/wasm/tx_claim_rewards/src/lib.rs b/wasm/tx_claim_rewards/src/lib.rs index eedd62c231..4ad1172be6 100644 --- a/wasm/tx_claim_rewards/src/lib.rs +++ b/wasm/tx_claim_rewards/src/lib.rs @@ -1,15 +1,19 @@ //! A tx for a user to claim PoS inflationary rewards due to bonds used as //! voting power in consensus. +use namada_tx_prelude::transaction::pos; use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: BatchedTx) -> TxResult { let data = ctx.get_tx_data(&tx_data)?; - let withdraw = transaction::pos::Withdraw::try_from_slice(&data[..]) - .wrap_err("Failed to decode Withdraw value")?; - - ctx.claim_reward_tokens(withdraw.source.as_ref(), &withdraw.validator) + let pos::ClaimRewards { + validator, + source, + receiver, + } = pos::ClaimRewards::try_from_slice(&data[..]) + .wrap_err("Failed to decode ClaimRewards value")?; + ctx.claim_reward_tokens(source.as_ref(), &validator, receiver.as_ref()) .wrap_err("Failed to claim rewards")?; Ok(()) diff --git a/wasm/vp_implicit/src/lib.rs b/wasm/vp_implicit/src/lib.rs index e15f9cb02f..59b00fd63a 100644 --- a/wasm/vp_implicit/src/lib.rs +++ b/wasm/vp_implicit/src/lib.rs @@ -79,8 +79,11 @@ fn validate_tx( source, validator, .. }) | PosAction::Withdraw(Withdraw { source, validator }) - | PosAction::ClaimRewards(ClaimRewards { validator, source }) => - { + | PosAction::ClaimRewards(ClaimRewards { + validator, + source, + receiver: _, + }) => { let source = source.unwrap_or(validator); gadget.verify_signatures_when( || source == addr, diff --git a/wasm/vp_user/src/lib.rs b/wasm/vp_user/src/lib.rs index 12d0d603bb..6bdd09efba 100644 --- a/wasm/vp_user/src/lib.rs +++ b/wasm/vp_user/src/lib.rs @@ -78,8 +78,11 @@ fn validate_tx( source, validator, .. }) | PosAction::Withdraw(Withdraw { source, validator }) - | PosAction::ClaimRewards(ClaimRewards { validator, source }) => - { + | PosAction::ClaimRewards(ClaimRewards { + validator, + source, + receiver: _, + }) => { let source = source.unwrap_or(validator); gadget.verify_signatures_when( || source == addr,