Skip to content
Merged
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
179 changes: 179 additions & 0 deletions clients/rust-legacy/tests/unwrap_lamports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,3 +424,182 @@ async fn self_owned_with_extensions() {
context.init_token_with_native_mint().await.unwrap();
run_self_owned_unwrap_lamports(context, TestMode::WithImmutableOwner).await;
}

async fn run_delegate_unwrap_lamports(context: TestContext, test_mode: TestMode) {
let TokenContext {
token, alice, bob, ..
} = context.token_context.unwrap();

let amount = 10000000000;
let account_space = match test_mode {
TestMode::Regular => Account::get_packed_len(),
TestMode::WithImmutableOwner => {
ExtensionType::try_calculate_account_len::<Account>(&[ExtensionType::ImmutableOwner])
.unwrap()
}
};

let rent_exempt_lamports = Rent::default().minimum_balance(account_space);

let alice_account = Keypair::new();
match test_mode {
TestMode::WithImmutableOwner => {
token
.wrap(
&alice_account.pubkey(),
&alice.pubkey(),
amount,
&[&alice_account],
)
.await
.unwrap();
}
TestMode::Regular => {
token
.wrap_with_mutable_ownership(
&alice_account.pubkey(),
&alice.pubkey(),
amount,
&[&alice_account],
)
.await
.unwrap();
}
}
let alice_account = alice_account.pubkey();
let bob_account = Keypair::new();
match test_mode {
TestMode::WithImmutableOwner => {
token
.wrap(
&bob_account.pubkey(),
&bob.pubkey(),
amount,
&[&bob_account],
)
.await
.unwrap();
}
TestMode::Regular => {
token
.wrap_with_mutable_ownership(
&bob_account.pubkey(),
&bob.pubkey(),
amount,
&[&bob_account],
)
.await
.unwrap();
}
}
let bob_account = bob_account.pubkey();

// set bob as a delegate of alice's account
token
.approve(
&alice_account,
&bob.pubkey(),
&alice.pubkey(),
amount,
&[&alice],
)
.await
.unwrap();

// unwrap Some(1) lamports with a delegate is ok
token
.unwrap_lamports(
&alice_account,
&bob_account,
&bob.pubkey(),
Some(1),
&[&bob],
)
.await
.unwrap();

let alice_account_account = token.get_account(alice_account).await.unwrap();
let alice_account_token_account = token.get_account_info(&alice_account).await.unwrap();
assert_eq!(alice_account_account.lamports, amount - 1);
assert_eq!(
alice_account_token_account.base.amount,
amount - (rent_exempt_lamports + 1)
);

let bob_account_account = token.get_account(bob_account).await.unwrap();
assert_eq!(bob_account_account.lamports, amount + 1);

// unwrap too much lamports is not ok
let error = token
.unwrap_lamports(
&alice_account,
&bob_account,
&bob.pubkey(),
Some(amount),
&[&bob],
)
.await
.unwrap_err();
assert_eq!(
error,
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InstructionError(
0,
InstructionError::Custom(TokenError::InsufficientFunds as u32)
)
)))
);

// wrong signer
let invalid_signer = Keypair::new();
let error = token
.unwrap_lamports(
&alice_account,
&bob_account,
&invalid_signer.pubkey(),
Some(1),
&[&invalid_signer],
)
.await
.unwrap_err();
assert_eq!(
error,
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InstructionError(
0,
InstructionError::Custom(TokenError::OwnerMismatch as u32)
)
)))
);

// unwrap None lamports with a delegate is ok
token
.unwrap_lamports(&alice_account, &bob_account, &bob.pubkey(), None, &[&bob])
.await
.unwrap();

let alice_account_account = token.get_account(alice_account).await.unwrap();
let alice_account_token_account = token.get_account_info(&alice_account).await.unwrap();
assert_eq!(alice_account_account.lamports, rent_exempt_lamports);
assert_eq!(alice_account_token_account.base.amount, 0);

let bob_account_account = token.get_account(bob_account).await.unwrap();
assert_eq!(
bob_account_account.lamports,
amount + (amount - rent_exempt_lamports)
);
}

#[tokio::test]
async fn delegate() {
let mut context = TestContext::new().await;
context.init_token_with_native_mint().await.unwrap();
run_delegate_unwrap_lamports(context, TestMode::Regular).await;
}

#[tokio::test]
async fn delegate_with_extensions() {
let mut context = TestContext::new().await;
context.init_token_with_native_mint().await.unwrap();
run_delegate_unwrap_lamports(context, TestMode::WithImmutableOwner).await;
}
8 changes: 6 additions & 2 deletions interface/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -742,12 +742,16 @@ pub enum TokenInstruction<'a> {
///
/// This is useful to unwrap lamports from a wrapped SOL account.
///
/// Accounts expected by this instruction:
///
/// * Single owner/delegate
/// 0. `[writable]` The source account.
/// 1. `[writable]` The destination account.
/// 2. `[signer]` The source account's owner/delegate.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Haha I didn't realize we even advertised it here

///
/// * Multisignature owner/delegate
/// 0. `[writable]` The source account.
/// 1. `[writable]` The destination account.
/// 2. `[]` The source account's multisignature owner/delegate.
/// 3. `..+M` `[signer]` M signer accounts.
UnwrapLamports {
/// The amount of lamports to transfer. When an amount is
/// not specified, the entire balance of the source account will be
Expand Down
41 changes: 34 additions & 7 deletions program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1770,13 +1770,40 @@ impl Processor {
return Err(TokenError::NonNativeNotSupported.into());
}

Self::validate_owner(
program_id,
&source_account.base.owner,
authority_info,
authority_info_data_len,
account_info_iter.as_slice(),
)?;
match source_account.base.delegate {
PodCOption {
option: PodCOption::<Pubkey>::SOME,
value: delegate,
} if authority_info.key == &delegate => {
Self::validate_owner(
program_id,
&delegate,
authority_info,
authority_info_data_len,
account_info_iter.as_slice(),
)?;

let delegated_amount = u64::from(source_account.base.delegated_amount);

source_account.base.delegated_amount = delegated_amount
.checked_sub(amount)
.ok_or(TokenError::InsufficientFunds)?
.into();

if u64::from(source_account.base.delegated_amount) == 0 {
source_account.base.delegate = PodCOption::none();
}
}
_ => {
Self::validate_owner(
program_id,
&source_account.base.owner,
authority_info,
authority_info_data_len,
account_info_iter.as_slice(),
)?;
}
}

if let Ok(cpi_guard) = source_account.get_extension::<CpiGuard>() {
if cpi_guard.lock_cpi.into() && in_cpi() {
Expand Down
Loading