@@ -3,8 +3,18 @@ import { describe, it, beforeEach, afterEach, expect, vi } from "vitest";
33import { serverUrl } from "../../../constants" ;
44import { StellarSdk } from "../../../test-utils/stellar-sdk-import" ;
55
6- const { Account, Keypair, rpc, contract, SorobanDataBuilder, xdr, Address } =
7- StellarSdk ;
6+ const {
7+ Account,
8+ Keypair,
9+ Operation,
10+ TransactionBuilder,
11+ TimeoutInfinite,
12+ rpc,
13+ contract,
14+ SorobanDataBuilder,
15+ xdr,
16+ Address,
17+ } = StellarSdk ;
818const { Server } = StellarSdk . rpc ;
919
1020const restoreTxnData = StellarSdk . SorobanDataBuilder . fromXDR (
@@ -155,3 +165,181 @@ describe("AssembledTransaction", () => {
155165 ) ;
156166 } ) ;
157167} ) ;
168+
169+ describe ( "Contract ID validation on deserialization" , ( ) => {
170+ const networkPassphrase = "Standalone Network ; February 2017" ;
171+ const keypair = Keypair . random ( ) ;
172+ const source = new Account ( keypair . publicKey ( ) , "0" ) ;
173+
174+ const victimContractId =
175+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" ;
176+ const attackerContractId =
177+ "CC53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53WQD5" ;
178+
179+ const createSpec = ( methodName : string ) => {
180+ const funcSpec = xdr . ScSpecEntry . scSpecEntryFunctionV0 (
181+ new xdr . ScSpecFunctionV0 ( {
182+ doc : "" ,
183+ name : methodName ,
184+ inputs : [ ] ,
185+ outputs : [ xdr . ScSpecTypeDef . scSpecTypeU32 ( ) ] ,
186+ } ) ,
187+ ) ;
188+ return new contract . Spec ( [ funcSpec . toXDR ( "base64" ) ] ) ;
189+ } ;
190+
191+ function buildInvokeTx ( targetContractId : string , methodName : string ) {
192+ return new TransactionBuilder ( source , {
193+ fee : "100" ,
194+ networkPassphrase,
195+ } )
196+ . setTimeout ( TimeoutInfinite )
197+ . addOperation (
198+ Operation . invokeContractFunction ( {
199+ contract : targetContractId ,
200+ function : methodName ,
201+ args : [ ] ,
202+ } ) ,
203+ )
204+ . build ( ) ;
205+ }
206+
207+ it ( "fromXDR() accepts a transaction targeting the configured contract" , ( ) => {
208+ const tx = buildInvokeTx ( victimContractId , "test" ) ;
209+ const xdrBase64 = tx . toEnvelope ( ) . toXDR ( "base64" ) ;
210+ const spec = createSpec ( "test" ) ;
211+
212+ const assembled = contract . AssembledTransaction . fromXDR (
213+ {
214+ contractId : victimContractId ,
215+ networkPassphrase,
216+ rpcUrl : "https://example.com" ,
217+ } ,
218+ xdrBase64 ,
219+ spec ,
220+ ) ;
221+ expect ( assembled . built ) . toBeDefined ( ) ;
222+ } ) ;
223+
224+ it ( "fromXDR() rejects a transaction targeting a different contract" , ( ) => {
225+ const tx = buildInvokeTx ( attackerContractId , "drain" ) ;
226+ const xdrBase64 = tx . toEnvelope ( ) . toXDR ( "base64" ) ;
227+ const spec = createSpec ( "drain" ) ;
228+
229+ expect ( ( ) =>
230+ contract . AssembledTransaction . fromXDR (
231+ {
232+ contractId : victimContractId ,
233+ networkPassphrase,
234+ rpcUrl : "https://example.com" ,
235+ } ,
236+ xdrBase64 ,
237+ spec ,
238+ ) ,
239+ ) . toThrow (
240+ `Transaction envelope targets contract ${ attackerContractId } , but this Client is configured for ${ victimContractId } .` ,
241+ ) ;
242+ } ) ;
243+
244+ it ( "fromJSON() accepts a transaction targeting the configured contract" , ( ) => {
245+ const tx = buildInvokeTx ( victimContractId , "test" ) ;
246+ const spec = createSpec ( "test" ) ;
247+ const simulationResult = {
248+ auth : [ ] ,
249+ retval : xdr . ScVal . scvU32 ( 0 ) . toXDR ( "base64" ) ,
250+ } ;
251+ const simulationTransactionData = new SorobanDataBuilder ( )
252+ . build ( )
253+ . toXDR ( "base64" ) ;
254+
255+ const json = JSON . stringify ( {
256+ method : "test" ,
257+ tx : tx . toEnvelope ( ) . toXDR ( "base64" ) ,
258+ simulationResult,
259+ simulationTransactionData,
260+ } ) ;
261+
262+ const { method, ...txData } = JSON . parse ( json ) ;
263+ const assembled = contract . AssembledTransaction . fromJSON (
264+ {
265+ contractId : victimContractId ,
266+ networkPassphrase,
267+ rpcUrl : "https://example.com" ,
268+ method,
269+ parseResultXdr : ( result : any ) => spec . funcResToNative ( method , result ) ,
270+ } ,
271+ txData ,
272+ ) ;
273+ expect ( assembled . built ) . toBeDefined ( ) ;
274+ } ) ;
275+
276+ it ( "fromJSON() rejects a transaction targeting a different contract" , ( ) => {
277+ const tx = buildInvokeTx ( attackerContractId , "drain" ) ;
278+ const simulationResult = {
279+ auth : [ ] ,
280+ retval : xdr . ScVal . scvU32 ( 0 ) . toXDR ( "base64" ) ,
281+ } ;
282+ const simulationTransactionData = new SorobanDataBuilder ( )
283+ . build ( )
284+ . toXDR ( "base64" ) ;
285+
286+ const json = JSON . stringify ( {
287+ method : "drain" ,
288+ tx : tx . toEnvelope ( ) . toXDR ( "base64" ) ,
289+ simulationResult,
290+ simulationTransactionData,
291+ } ) ;
292+
293+ const { method, ...txData } = JSON . parse ( json ) ;
294+
295+ expect ( ( ) =>
296+ contract . AssembledTransaction . fromJSON (
297+ {
298+ contractId : victimContractId ,
299+ networkPassphrase,
300+ rpcUrl : "https://example.com" ,
301+ method,
302+ parseResultXdr : ( ) => { } ,
303+ } ,
304+ txData ,
305+ ) ,
306+ ) . toThrow (
307+ `Transaction envelope targets contract ${ attackerContractId } , but this Client is configured for ${ victimContractId } .` ,
308+ ) ;
309+ } ) ;
310+
311+ it ( "fromJSON() rejects a transaction with a spoofed method name" , ( ) => {
312+ const tx = buildInvokeTx ( victimContractId , "transfer" ) ;
313+ const simulationResult = {
314+ auth : [ ] ,
315+ retval : xdr . ScVal . scvU32 ( 0 ) . toXDR ( "base64" ) ,
316+ } ;
317+ const simulationTransactionData = new SorobanDataBuilder ( )
318+ . build ( )
319+ . toXDR ( "base64" ) ;
320+
321+ const json = JSON . stringify ( {
322+ method : "safe_operation" ,
323+ tx : tx . toEnvelope ( ) . toXDR ( "base64" ) ,
324+ simulationResult,
325+ simulationTransactionData,
326+ } ) ;
327+
328+ const { method, ...txData } = JSON . parse ( json ) ;
329+
330+ expect ( ( ) =>
331+ contract . AssembledTransaction . fromJSON (
332+ {
333+ contractId : victimContractId ,
334+ networkPassphrase,
335+ rpcUrl : "https://example.com" ,
336+ method,
337+ parseResultXdr : ( ) => { } ,
338+ } ,
339+ txData ,
340+ ) ,
341+ ) . toThrow (
342+ "Transaction envelope calls method 'transfer', but the provided method is 'safe_operation'." ,
343+ ) ;
344+ } ) ;
345+ } ) ;
0 commit comments