-
Notifications
You must be signed in to change notification settings - Fork 530
security: fix broken take_offer invariant + add missing RefundOffer instruction #668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
NikkiAung
wants to merge
3
commits into
solana-foundation:main
Choose a base branch
from
NikkiAung:fix/escrow-refund-offer
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
87 changes: 87 additions & 0 deletions
87
tokens/escrow/anchor/programs/escrow/src/instructions/refund_offer.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| use anchor_lang::prelude::*; | ||
|
|
||
| use anchor_spl::token_interface::{ | ||
| close_account, transfer_checked, CloseAccount, Mint, TokenAccount, TokenInterface, | ||
| TransferChecked, | ||
| }; | ||
|
|
||
| use crate::Offer; | ||
|
|
||
| #[derive(Accounts)] | ||
| pub struct RefundOffer<'info> { | ||
| #[account(mut)] | ||
| pub maker: Signer<'info>, | ||
|
|
||
| pub token_mint_a: InterfaceAccount<'info, Mint>, | ||
|
|
||
| #[account( | ||
| mut, | ||
| associated_token::mint = token_mint_a, | ||
| associated_token::authority = maker, | ||
| associated_token::token_program = token_program, | ||
| )] | ||
| pub maker_token_account_a: InterfaceAccount<'info, TokenAccount>, | ||
|
|
||
| #[account( | ||
| mut, | ||
| close = maker, | ||
| has_one = maker, | ||
| has_one = token_mint_a, | ||
| seeds = [b"offer", maker.key().as_ref(), offer.id.to_le_bytes().as_ref()], | ||
| bump = offer.bump | ||
| )] | ||
| offer: Account<'info, Offer>, | ||
|
|
||
| #[account( | ||
| mut, | ||
| associated_token::mint = token_mint_a, | ||
| associated_token::authority = offer, | ||
| associated_token::token_program = token_program, | ||
| )] | ||
| pub vault: InterfaceAccount<'info, TokenAccount>, | ||
|
|
||
| pub token_program: Interface<'info, TokenInterface>, | ||
| } | ||
|
|
||
| pub fn withdraw_and_close_vault_for_refund(ctx: Context<RefundOffer>) -> Result<()> { | ||
| let seeds = &[ | ||
| b"offer", | ||
| ctx.accounts.maker.to_account_info().key.as_ref(), | ||
| &ctx.accounts.offer.id.to_le_bytes()[..], | ||
| &[ctx.accounts.offer.bump], | ||
| ]; | ||
| let signer_seeds = [&seeds[..]]; | ||
|
|
||
| let accounts = TransferChecked { | ||
| from: ctx.accounts.vault.to_account_info(), | ||
| mint: ctx.accounts.token_mint_a.to_account_info(), | ||
| to: ctx.accounts.maker_token_account_a.to_account_info(), | ||
| authority: ctx.accounts.offer.to_account_info(), | ||
| }; | ||
|
|
||
| let cpi_context = CpiContext::new_with_signer( | ||
| ctx.accounts.token_program.key(), | ||
| accounts, | ||
| &signer_seeds, | ||
| ); | ||
|
|
||
| transfer_checked( | ||
| cpi_context, | ||
| ctx.accounts.vault.amount, | ||
| ctx.accounts.token_mint_a.decimals, | ||
| )?; | ||
|
|
||
| let accounts = CloseAccount { | ||
| account: ctx.accounts.vault.to_account_info(), | ||
| destination: ctx.accounts.maker.to_account_info(), | ||
| authority: ctx.accounts.offer.to_account_info(), | ||
| }; | ||
|
|
||
| let cpi_context = CpiContext::new_with_signer( | ||
| ctx.accounts.token_program.key(), | ||
| accounts, | ||
| &signer_seeds, | ||
| ); | ||
|
|
||
| close_account(cpi_context) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,6 @@ pub use make_offer::*; | |
|
|
||
| pub mod take_offer; | ||
| pub use take_offer::*; | ||
|
|
||
| pub mod refund_offer; | ||
| pub use refund_offer::*; | ||
118 changes: 118 additions & 0 deletions
118
tokens/escrow/native/program/src/instructions/refund_offer.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| use { | ||
| crate::{error::*, state::*, utils::*}, | ||
| borsh::BorshDeserialize, | ||
| solana_program::{ | ||
| account_info::AccountInfo, entrypoint::ProgramResult, program::invoke_signed, program_error::ProgramError, | ||
| program_pack::Pack, pubkey::Pubkey, | ||
| }, | ||
| spl_token_interface::{instruction as token_instruction, state::Account as TokenAccount}, | ||
| }; | ||
|
|
||
| #[derive(BorshDeserialize, Debug)] | ||
| pub struct RefundOffer {} | ||
|
|
||
| impl RefundOffer { | ||
| pub fn process(program_id: &Pubkey, accounts: &[AccountInfo<'_>]) -> ProgramResult { | ||
| // accounts in order | ||
| // | ||
| let [ | ||
| offer_info, // offer account info | ||
| token_mint_a, // token mint a | ||
| maker_token_account_a, // maker token a account, receives the refund | ||
| vault, // vault | ||
| maker, // maker | ||
| token_program, // token program | ||
| system_program// system program | ||
| ] = accounts else { | ||
| return Err(ProgramError::NotEnoughAccountKeys); | ||
| }; | ||
|
|
||
| // ensure the maker signs the instruction | ||
| // | ||
| if !maker.is_signer { | ||
| return Err(ProgramError::MissingRequiredSignature); | ||
| } | ||
|
|
||
| // ensure the caller didn't substitute a fake token program - the real | ||
| // program is what actually enforces the transfer/close below | ||
| // | ||
| spl_token_interface::check_program_account(token_program.key)?; | ||
|
|
||
| // get the offer data | ||
| // | ||
| let offer = Offer::try_from_slice(&offer_info.data.borrow()[..])?; | ||
|
|
||
| // only the maker who created the offer may refund it | ||
| // | ||
| assert_eq!(&offer.maker, maker.key); | ||
| assert_eq!(&offer.token_mint_a, token_mint_a.key); | ||
|
|
||
| // validate the offer account with signer seeds | ||
| // | ||
| let offer_signer_seeds = &[Offer::SEED_PREFIX, maker.key.as_ref(), &offer.id.to_le_bytes(), &[offer.bump]]; | ||
|
|
||
| let offer_key = Pubkey::create_program_address(offer_signer_seeds, program_id)?; | ||
|
|
||
| // make sure the offer key is the same | ||
| // | ||
| if *offer_info.key != offer_key { | ||
| return Err(EscrowError::OfferKeyMismatch.into()); | ||
| }; | ||
|
|
||
| // validate the maker's receiving address | ||
| // | ||
| assert_is_associated_token_account(maker_token_account_a.key, maker.key, token_mint_a.key)?; | ||
|
|
||
| // validate the vault is the offer's actual vault, not a substitute | ||
| // token-A account that also happens to be owned by the offer PDA | ||
| // | ||
| assert_is_associated_token_account(vault.key, offer_info.key, token_mint_a.key)?; | ||
|
|
||
| // return the vaulted tokens to the maker | ||
| // | ||
| let vault_amount_a = TokenAccount::unpack(&vault.data.borrow())?.amount; | ||
|
|
||
| invoke_signed( | ||
| &token_instruction::transfer( | ||
| token_program.key, | ||
| vault.key, | ||
| maker_token_account_a.key, | ||
| offer_info.key, | ||
| &[offer_info.key], | ||
| vault_amount_a, | ||
| )?, | ||
| &[vault.clone(), maker_token_account_a.clone(), offer_info.clone(), token_program.clone()], | ||
| &[offer_signer_seeds], | ||
| )?; | ||
|
|
||
| // close the vault account, rent to the maker | ||
| // | ||
| invoke_signed( | ||
| &spl_token_interface::instruction::close_account( | ||
| token_program.key, | ||
| vault.key, | ||
| maker.key, | ||
| offer_info.key, | ||
| &[], | ||
| )?, | ||
| &[vault.clone(), maker.clone(), offer_info.clone()], | ||
| &[offer_signer_seeds], | ||
| )?; | ||
|
|
||
| // Send the rent back to the maker | ||
| // | ||
| let lamports = offer_info.lamports(); | ||
| **offer_info.lamports.borrow_mut() -= lamports; | ||
| **maker.lamports.borrow_mut() += lamports; | ||
|
|
||
| // Realloc the account to zero | ||
| // | ||
| offer_info.resize(0)?; | ||
|
|
||
| // Assign the account to the System Program | ||
| // | ||
| offer_info.assign(system_program.key); | ||
|
|
||
| Ok(()) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the maker supplies another executable account as
token_program, both CPIs can report success without transferring or closing the vault, after which the handler destroys the offer account and leaves its deposited tokens without a recovery path.How this was verified: The caller-supplied program key is used for both CPIs, and the offer is then closed without independently checking that the vault was drained.
Knowledge Base Used: Tokens Directory Overview