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
8 changes: 4 additions & 4 deletions program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ pub enum TokenWrapInstruction {

/// Wrap tokens
///
/// Move a user's unwrapped tokens into an escrow account and mint the same
/// number of wrapped tokens into the provided account.
/// Move a user's unwrapped tokens into an escrow account and mint wrapped
/// tokens into the provided account at the wrapped supply / escrow rate.
///
/// Accounts expected by this instruction:
///
Expand Down Expand Up @@ -65,8 +65,8 @@ pub enum TokenWrapInstruction {

/// Unwrap tokens
///
/// Burn user wrapped tokens and transfer the same amount of unwrapped
/// tokens from the escrow account to the provided account.
/// Burn user wrapped tokens and transfer unwrapped tokens from the escrow
/// account to the provided account at the escrow / wrapped supply rate.
///
/// Accounts expected by this instruction:
/// 0. `[w]` Escrow of unwrapped tokens, address must be an `ATA`:
Expand Down
36 changes: 35 additions & 1 deletion program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,33 @@ pub fn process_create_mint<M: MintCustomizer>(
Ok(())
}

/// Returns the escrowed reserve and the wrapped supply
fn reserve_and_supply(
escrow: &AccountInfo,
wrapped_mint: &AccountInfo,
) -> Result<(u128, u128), ProgramError> {
let escrow_data = escrow.try_borrow_data()?;
let mint_data = wrapped_mint.try_borrow_data()?;
let reserve = PodStateWithExtensions::<PodAccount>::unpack(&escrow_data)?
.base
.amount;
let supply = PodStateWithExtensions::<PodMint>::unpack(&mint_data)?
.base
.supply;
Ok((u64::from(reserve).into(), u64::from(supply).into()))
}

/// Converts `amount` at `numerator / denominator`, rounding down so that
/// reserve per wrapped token never falls
fn convert(amount: u64, numerator: u128, denominator: u128) -> Result<u64, ProgramError> {
u128::from(amount)
.checked_mul(numerator)
.and_then(|product| product.checked_div(denominator))
.and_then(|converted| u64::try_from(converted).ok())
.filter(|converted| *converted != 0)
.ok_or(ProgramError::ArithmeticOverflow)
}

/// Processes [`Wrap`](enum.TokenWrapInstruction.html) instruction.
pub fn process_wrap(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
if amount == 0 {
Expand Down Expand Up @@ -263,6 +290,10 @@ pub fn process_wrap(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let net_amount = amount
.checked_sub(fee)
.ok_or(ProgramError::ArithmeticOverflow)?;
let net_amount = match reserve_and_supply(unwrapped_escrow, wrapped_mint)? {
(_, 0) => net_amount,
(reserve, supply) => convert(net_amount, supply, reserve)?,
};

if unwrapped_token_program.key == &spl_token_2022_interface::id() {
// This invoke fn does extra validation on calculated fee
Expand Down Expand Up @@ -357,6 +388,9 @@ pub fn process_unwrap(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
Err(TokenWrapError::EscrowMismatch)?
}

let (reserve, supply) = reserve_and_supply(unwrapped_escrow, wrapped_mint)?;
let unwrapped_amount = convert(amount, reserve, supply)?;

// Burn wrapped tokens

let multisig_signer_keys = extract_multisig_accounts(transfer_authority, additional_accounts)?
Expand Down Expand Up @@ -390,7 +424,7 @@ pub fn process_unwrap(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
recipient_unwrapped_token.clone(),
wrapped_mint_authority.clone(),
additional_accounts,
amount,
unwrapped_amount,
unwrapped_mint_state.base.decimals,
&[&signer_seeds],
)?;
Expand Down
1 change: 1 addition & 0 deletions program/tests/helpers/unwrap_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ impl<'a> UnwrapBuilder<'a> {
account: MintBuilder::new()
.token_program(token_program)
.mint_authority(mint_authority)
.supply(self.escrow_starting_amount.unwrap_or(100_000))
.build()
.account,
})
Expand Down
1 change: 1 addition & 0 deletions program/tests/helpers/wrap_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ impl<'a> WrapBuilder<'a> {
account: MintBuilder::new()
.token_program(token_program)
.mint_authority(mint_authority)
.supply(0)
.build()
.account,
})
Expand Down
38 changes: 36 additions & 2 deletions program/tests/test_unwrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use {
crate::helpers::{
common::{
setup_counter, setup_multisig, setup_validation_state_account, KeyedAccount,
TokenProgram, DEFAULT_MINT_SUPPLY,
TokenProgram,
},
create_mint_builder::CreateMintBuilder,
extensions::MintExtension::{
Expand Down Expand Up @@ -112,7 +112,7 @@ fn assert_unwrap_result(
.unwrap();
assert_eq!(
u64::from(mint.base.supply),
DEFAULT_MINT_SUPPLY.checked_sub(unwrap_amount).unwrap()
escrow_starting_amount.checked_sub(unwrap_amount).unwrap()
);

// Verify escrow was debited
Expand Down Expand Up @@ -495,3 +495,37 @@ fn test_unwrap_with_confidential_transfer_mint() {
&unwrap_result,
);
}

#[test]
fn test_unwrap_pays_at_escrow_per_supply_rate() {
let unwrapped_mint = KeyedAccount {
key: Pubkey::new_unique(),
account: MintBuilder::new().build().account,
};
let wrapped_mint_address =
get_wrapped_mint_address(&unwrapped_mint.key, &spl_token_2022_interface::id());
let wrapped_mint = KeyedAccount {
key: wrapped_mint_address,
account: MintBuilder::new()
.token_program(TokenProgram::SplToken2022)
.mint_authority(get_wrapped_mint_authority(&wrapped_mint_address))
.supply(100_000)
.build()
.account,
};

let unwrap_result = UnwrapBuilder::default()
.unwrapped_mint(unwrapped_mint)
.wrapped_mint(wrapped_mint)
.escrow_starting_amount(150_001)
.unwrap_amount(500)
.check(Check::success())
.execute();

// 500 * 150_001 / 100_000 rounds down
let recipient = PodStateWithExtensions::<PodAccount>::unpack(
&unwrap_result.recipient_unwrapped_token.account.data,
)
.unwrap();
assert_eq!(u64::from(recipient.base.amount), 750);
}
48 changes: 43 additions & 5 deletions program/tests/test_wrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use {
crate::helpers::{
common::{
setup_counter, setup_multisig, setup_validation_state_account, KeyedAccount,
TokenProgram, DEFAULT_MINT_SUPPLY,
TokenProgram,
},
create_mint_builder::CreateMintBuilder,
extensions::MintExtension::{
Expand Down Expand Up @@ -112,10 +112,7 @@ fn assert_wrap_result(starting_amount: u64, wrap_amount: u64, wrap_result: &Wrap
// Verify wrapped mint supply increased
let mint =
PodStateWithExtensions::<PodMint>::unpack(&wrap_result.wrapped_mint.account.data).unwrap();
assert_eq!(
u64::from(mint.base.supply),
DEFAULT_MINT_SUPPLY.checked_add(wrap_amount).unwrap()
);
assert_eq!(u64::from(mint.base.supply), wrap_amount);
}

#[test]
Expand Down Expand Up @@ -430,3 +427,44 @@ fn test_wrap_with_confidential_transfer_mint() {

assert_wrap_result(starting_amount, wrap_amount, &wrap_result);
}

#[test]
fn test_wrap_mints_at_supply_per_escrow_rate() {
let unwrapped_mint = KeyedAccount {
key: Pubkey::new_unique(),
account: MintBuilder::new().build().account,
};
let wrapped_mint_address =
get_wrapped_mint_address(&unwrapped_mint.key, &spl_token_2022_interface::id());
let wrapped_mint_authority = get_wrapped_mint_authority(&wrapped_mint_address);
let wrapped_mint = KeyedAccount {
key: wrapped_mint_address,
account: MintBuilder::new()
.token_program(TokenProgram::SplToken2022)
.mint_authority(wrapped_mint_authority)
.supply(100_000)
.build()
.account,
};
let escrow = TokenAccountBuilder::new()
.mint(unwrapped_mint.clone())
.owner(wrapped_mint_authority)
.amount(150_001)
.build()
.account;

let wrap_result = WrapBuilder::default()
.unwrapped_mint(unwrapped_mint)
.wrapped_mint(wrapped_mint)
.unwrapped_escrow_account(escrow)
.wrap_amount(750)
.check(Check::success())
.execute();

// 750 * 100_000 / 150_001 rounds down
let recipient = PodStateWithExtensions::<PodAccount>::unpack(
&wrap_result.recipient_wrapped_token.account.data,
)
.unwrap();
assert_eq!(u64::from(recipient.base.amount), 499);
}