diff --git a/program/src/instruction.rs b/program/src/instruction.rs index b6fa1863..7ea489df 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -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: /// @@ -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`: diff --git a/program/src/processor.rs b/program/src/processor.rs index c0a41c87..e12662ef 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -199,6 +199,33 @@ pub fn process_create_mint( 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::::unpack(&escrow_data)? + .base + .amount; + let supply = PodStateWithExtensions::::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 { + 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 { @@ -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 @@ -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)? @@ -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], )?; diff --git a/program/tests/helpers/unwrap_builder.rs b/program/tests/helpers/unwrap_builder.rs index 408211d6..b63a8fc3 100644 --- a/program/tests/helpers/unwrap_builder.rs +++ b/program/tests/helpers/unwrap_builder.rs @@ -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, }) diff --git a/program/tests/helpers/wrap_builder.rs b/program/tests/helpers/wrap_builder.rs index 5b7c4878..56600ac2 100644 --- a/program/tests/helpers/wrap_builder.rs +++ b/program/tests/helpers/wrap_builder.rs @@ -147,6 +147,7 @@ impl<'a> WrapBuilder<'a> { account: MintBuilder::new() .token_program(token_program) .mint_authority(mint_authority) + .supply(0) .build() .account, }) diff --git a/program/tests/test_unwrap.rs b/program/tests/test_unwrap.rs index 6e4845b1..319af0d8 100644 --- a/program/tests/test_unwrap.rs +++ b/program/tests/test_unwrap.rs @@ -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::{ @@ -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 @@ -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::::unpack( + &unwrap_result.recipient_unwrapped_token.account.data, + ) + .unwrap(); + assert_eq!(u64::from(recipient.base.amount), 750); +} diff --git a/program/tests/test_wrap.rs b/program/tests/test_wrap.rs index b7835f95..066a70c3 100644 --- a/program/tests/test_wrap.rs +++ b/program/tests/test_wrap.rs @@ -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::{ @@ -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::::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] @@ -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::::unpack( + &wrap_result.recipient_wrapped_token.account.data, + ) + .unwrap(); + assert_eq!(u64::from(recipient.base.amount), 499); +}