11import { describe , test } from 'node:test' ;
22import { AccountLayout } from '@solana/spl-token' ;
3- import { Transaction } from '@solana/web3.js' ;
3+ import { Keypair , SystemProgram , Transaction } from '@solana/web3.js' ;
44import { assert } from 'chai' ;
55import { start } from 'solana-bankrun' ;
66import { OfferAccount } from './account' ;
@@ -15,6 +15,11 @@ describe('Escrow!', async () => {
1515 const client = context . banksClient ;
1616 const payer = context . payer ;
1717
18+ // Used to reproduce the "rent refund to arbitrary payer" bug: provide a
19+ // different signer as `payer` for TakeOffer.
20+ const attacker = Keypair . generate ( ) ;
21+ let offerRentLamports = 0n ;
22+
1823 console . log ( `Program Address : ${ values . programId } ` ) ;
1924 console . log ( `Payer Address : ${ payer . publicKey } ` ) ;
2025
@@ -57,6 +62,7 @@ describe('Escrow!', async () => {
5762 await client . processTransaction ( tx ) ;
5863
5964 const offerInfo = await client . getAccount ( values . offer ) ;
65+ offerRentLamports = BigInt ( offerInfo . lamports ) ;
6066 const offer = OfferAccount . fromBuffer ( offerInfo . data ) . toData ( ) ;
6167
6268 const vaultInfo = await client . getAccount ( values . vault ) ;
@@ -70,7 +76,34 @@ describe('Escrow!', async () => {
7076 assert ( vaultTokenAccount . amount . toString ( ) === values . amountA . toString ( ) , 'unexpected amount A' ) ;
7177 } ) ;
7278
73- test ( 'Take Offer' , async ( ) => {
79+ test ( 'Take Offer (rent refunded to maker, not arbitrary payer)' , async ( ) => {
80+ // Ensure system accounts exist with known starting balances.
81+ // (Bankrun doesn't materialize system accounts until they hold lamports.)
82+ const fundTx = new Transaction ( ) ;
83+ fundTx . recentBlockhash = context . lastBlockhash ;
84+ fundTx . feePayer = payer . publicKey ;
85+ fundTx
86+ . add (
87+ SystemProgram . transfer ( {
88+ fromPubkey : payer . publicKey ,
89+ toPubkey : attacker . publicKey ,
90+ lamports : 5_000_000 ,
91+ } ) ,
92+ )
93+ . add (
94+ SystemProgram . transfer ( {
95+ fromPubkey : payer . publicKey ,
96+ toPubkey : values . maker . publicKey ,
97+ lamports : 5_000_000 ,
98+ } ) ,
99+ )
100+ . sign ( payer ) ;
101+ await client . processTransaction ( fundTx ) ;
102+
103+ const makerBefore = BigInt ( ( await client . getAccount ( values . maker . publicKey ) ) . lamports ) ;
104+ const attackerBefore = BigInt ( ( await client . getAccount ( attacker . publicKey ) ) . lamports ) ;
105+
106+ // Build TakeOffer with an arbitrary attacker-controlled payer.
74107 const ix = buildTakeOffer ( {
75108 maker : values . maker . publicKey ,
76109 offer : values . offer ,
@@ -81,17 +114,23 @@ describe('Escrow!', async () => {
81114 taker : values . taker . publicKey ,
82115 taker_token_a : values . takerAccountA ,
83116 taker_token_b : values . takerAccountB ,
84- payer : payer . publicKey ,
117+ payer : attacker . publicKey ,
85118 programId : values . programId ,
86119 } ) ;
87120
88- const blockhash = context . lastBlockhash ;
89-
90121 const tx = new Transaction ( ) ;
91- tx . recentBlockhash = blockhash ;
92- tx . add ( ix ) . sign ( payer , values . taker ) ;
122+ tx . recentBlockhash = context . lastBlockhash ;
123+ tx . feePayer = payer . publicKey ; // keep fees deterministic and away from attacker
124+ tx . add ( ix ) . sign ( payer , attacker , values . taker ) ;
93125 await client . processTransaction ( tx ) ;
94126
127+ const makerAfter = BigInt ( ( await client . getAccount ( values . maker . publicKey ) ) . lamports ) ;
128+ const attackerAfter = BigInt ( ( await client . getAccount ( attacker . publicKey ) ) . lamports ) ;
129+
130+ // Security property: offer account rent must not be transferable to an arbitrary `payer`.
131+ assert ( makerAfter - makerBefore === offerRentLamports , 'maker did not receive offer rent' ) ;
132+ assert ( attackerAfter <= attackerBefore , 'attacker unexpectedly received offer rent' ) ;
133+
95134 const offerInfo = await client . getAccount ( values . offer ) ;
96135 assert ( offerInfo === null , 'offer account not closed' ) ;
97136
0 commit comments