Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use anchor_lang::prelude::*;
use anchor_spl::{
token_2022::{close_account, transfer_checked, CloseAccount, TransferChecked},
token_interface::{Mint, TokenAccount, TokenInterface},
};

use crate::Offer;

#[derive(Accounts)]
#[instruction(id: u64)]
pub struct CancelOffer<'a> {
#[account(mut)]
pub maker: Signer<'a>,

#[account(
mut,
close = maker,
seeds = [b"offer", maker.key().as_ref(), &id.to_le_bytes()],
bump = offer.bump,
has_one = maker,
has_one = token_mint_a
)]
pub offer: Account<'a, Offer>,

#[account(
mut,
associated_token::authority = offer,
associated_token::mint = token_mint_a,
associated_token::token_program = token_program,
)]
pub vault: InterfaceAccount<'a, TokenAccount>,

#[account(
mut,
associated_token::mint = token_mint_a,
associated_token::authority = maker,
associated_token::token_program = token_program,
)]
pub maker_token_account_a: InterfaceAccount<'a, TokenAccount>,

#[account(mint::token_program = token_program)]
pub token_mint_a: InterfaceAccount<'a, Mint>,

pub token_program: Interface<'a, TokenInterface>,
pub system_program: Program<'a, System>,
}

pub fn handler(ctx: Context<CancelOffer>, id: u64) -> Result<()> {
// 1. Transfer Token A from the vault back to the maker
let seeds = &[
b"offer",
ctx.accounts.maker.to_account_info().key.as_ref(),
&id.to_le_bytes(),
&[ctx.accounts.offer.bump],
];

let signer_seeds = &[&seeds[..]];

let transfer_accounts = TransferChecked {
from: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.maker_token_account_a.to_account_info(),
authority: ctx.accounts.offer.to_account_info(),
mint: ctx.accounts.token_mint_a.to_account_info(),
};

let cpi_cotext = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
transfer_accounts,
signer_seeds,
);

transfer_checked(
cpi_cotext,
ctx.accounts.vault.amount,
ctx.accounts.token_mint_a.decimals,
)?;

// 2. Close the vault account and reclaim rent to the maker
let close_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.to_account_info(),
close_accounts,
signer_seeds,
);
close_account(cpi_context)?;
Ok(())
}
3 changes: 3 additions & 0 deletions tokens/escrow/anchor/programs/escrow/src/instructions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ pub use take_offer::*;

pub mod shared;
pub use shared::*;

pub mod cancel_offer;
pub use cancel_offer::*;
4 changes: 4 additions & 0 deletions tokens/escrow/anchor/programs/escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,8 @@ pub mod escrow {
instructions::take_offer::send_wanted_tokens_to_maker(&context)?;
instructions::take_offer::withdraw_and_close_vault(context)
}

pub fn cancel_offer(ctx: Context<CancelOffer>, id: u64) -> Result<()> {
instructions::cancel_offer::handler(ctx, id)
}
}
63 changes: 63 additions & 0 deletions tokens/escrow/anchor/tests/escrow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,67 @@ describe('escrow', async () => {
const aliceTokenAccountBalanceAfter = new BN(aliceTokenAccountBalanceAfterResponse.value.amount);
assert(aliceTokenAccountBalanceAfter.eq(tokenBWantedAmount));
}).slow(ANCHOR_SLOW_TEST_THRESHOLD);

it('Allows Alice to cancel an offer and reclaim her tokens', async () => {
// Pick a random ID for the offer we'll make
const offerId = getRandomBigNumber();

// Then determine the account addresses we'll use for the offer and the vault
const offer = PublicKey.findProgramAddressSync(
[
Buffer.from('offer'),
accounts.maker.toBuffer(),
offerId.toArrayLike(Buffer, 'le', 8),
],
program.programId,
)[0];

const vault = getAssociatedTokenAddressSync(
accounts.tokenMintA,
offer,
true,
TOKEN_PROGRAM,
);

accounts.offer = offer;
accounts.vault = vault;

// Get initial balance
const initialBalanceResponse = await connection.getTokenAccountBalance(
accounts.makerTokenAccountA,
);
const initialBalance = new BN(initialBalanceResponse.value.amount);

// Make the offer
await program.methods
.makeOffer(offerId, tokenAOfferedAmount, tokenBWantedAmount)
.accounts({ ...accounts })
.signers([alice])
.rpc();

// Check balance decreased
const postOfferBalanceResponse = await connection.getTokenAccountBalance(
accounts.makerTokenAccountA,
);
const postOfferBalance = new BN(postOfferBalanceResponse.value.amount);
assert(postOfferBalance.eq(initialBalance.sub(tokenAOfferedAmount)));

// Cancel the offer
await program.methods
.cancelOffer(offerId)
.accounts({ ...accounts })
.signers([alice])
.rpc();

// Check balance restored
const finalBalanceResponse = await connection.getTokenAccountBalance(
accounts.makerTokenAccountA,
);
const finalBalance = new BN(finalBalanceResponse.value.amount);
assert(finalBalance.eq(initialBalance));

// Check vault account is closed
const vaultAccountInfo = await connection.getAccountInfo(vault);
assert.isNull(vaultAccountInfo, 'Vault account should be closed');
}).slow(ANCHOR_SLOW_TEST_THRESHOLD);
});