Skip to content

Commit 446a255

Browse files
committed
Refactor
1 parent bd8636d commit 446a255

File tree

2 files changed

+98
-54
lines changed

2 files changed

+98
-54
lines changed

src/contract/assembled_transaction.ts

Lines changed: 63 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -361,53 +361,86 @@ export class AssembledTransaction<T> {
361361
});
362362
}
363363

364-
static fromJSON<T>(
365-
options: Omit<AssembledTransactionOptions<T>, "args">,
366-
{
367-
tx,
368-
simulationResult,
369-
simulationTransactionData,
370-
}: {
371-
tx: XDR_BASE64;
372-
simulationResult: {
373-
auth: XDR_BASE64[];
374-
retval: XDR_BASE64;
375-
};
376-
simulationTransactionData: XDR_BASE64;
377-
},
378-
): AssembledTransaction<T> {
379-
const txn = new AssembledTransaction(options);
380-
txn.built = TransactionBuilder.fromXDR(tx, options.networkPassphrase) as Tx;
381-
382-
if (txn.built.operations.length !== 1) {
364+
/**
365+
* Validate that a built transaction is a single invokeContract operation
366+
* targeting the expected contract, and return the parsed InvokeContractArgs.
367+
*/
368+
private static validateInvokeContractOp(
369+
built: Tx,
370+
expectedContractId: string,
371+
): xdr.InvokeContractArgs {
372+
if (built.operations.length !== 1) {
383373
throw new Error(
384374
"Transaction envelope must contain exactly one operation.",
385375
);
386376
}
387377

388-
const operation = txn.built.operations[0] as Operation.InvokeHostFunction;
378+
const operation = built.operations[0] as Operation.InvokeHostFunction;
379+
380+
if (!operation?.func?.switch) {
381+
throw new Error(
382+
"Transaction envelope does not contain an invokeHostFunction operation.",
383+
);
384+
}
389385

390-
if (!operation?.func?.value || typeof operation.func.value !== "function") {
386+
if (operation.func.switch().name !== "hostFunctionTypeInvokeContract") {
391387
throw new Error(
392-
"Could not extract the method from the transaction envelope.",
388+
"Transaction envelope does not contain an invokeContract host function.",
393389
);
394390
}
395391

396392
const invokeContractArgs = operation.func.value() as xdr.InvokeContractArgs;
397393

398-
if (!invokeContractArgs?.functionName) {
394+
if (
395+
!invokeContractArgs?.contractAddress ||
396+
!invokeContractArgs?.functionName
397+
) {
399398
throw new Error(
400-
"Could not extract the method name from the transaction envelope.",
399+
"Could not extract contract address or method name from the transaction envelope.",
401400
);
402401
}
403402

404403
const xdrContractId = Address.fromScAddress(
405404
invokeContractArgs.contractAddress(),
406405
).toString();
407406

408-
if (xdrContractId !== options.contractId) {
407+
if (xdrContractId !== expectedContractId) {
408+
throw new Error(
409+
`Transaction envelope targets contract ${xdrContractId}, but this Client is configured for ${expectedContractId}.`,
410+
);
411+
}
412+
413+
return invokeContractArgs;
414+
}
415+
416+
static fromJSON<T>(
417+
options: Omit<AssembledTransactionOptions<T>, "args">,
418+
{
419+
tx,
420+
simulationResult,
421+
simulationTransactionData,
422+
}: {
423+
tx: XDR_BASE64;
424+
simulationResult: {
425+
auth: XDR_BASE64[];
426+
retval: XDR_BASE64;
427+
};
428+
simulationTransactionData: XDR_BASE64;
429+
},
430+
): AssembledTransaction<T> {
431+
const txn = new AssembledTransaction(options);
432+
txn.built = TransactionBuilder.fromXDR(tx, options.networkPassphrase) as Tx;
433+
434+
const invokeContractArgs = AssembledTransaction.validateInvokeContractOp(
435+
txn.built,
436+
options.contractId,
437+
);
438+
439+
const xdrMethod = invokeContractArgs.functionName().toString("utf-8");
440+
441+
if (xdrMethod !== options.method) {
409442
throw new Error(
410-
`Transaction envelope targets contract ${xdrContractId}, but this Client is configured for ${options.contractId}.`,
443+
`Transaction envelope calls method '${xdrMethod}', but the provided method is '${options.method}'.`,
411444
);
412445
}
413446

@@ -453,34 +486,10 @@ export class AssembledTransaction<T> {
453486
options.networkPassphrase,
454487
) as Tx;
455488

456-
if (built.operations.length !== 1) {
457-
throw new Error(
458-
"Transaction envelope must contain exactly one operation.",
459-
);
460-
}
461-
462-
const operation = built.operations[0] as Operation.InvokeHostFunction;
463-
if (!operation?.func?.value || typeof operation.func.value !== "function") {
464-
throw new Error(
465-
"Could not extract the method from the transaction envelope.",
466-
);
467-
}
468-
const invokeContractArgs = operation.func.value() as xdr.InvokeContractArgs;
469-
if (!invokeContractArgs?.functionName) {
470-
throw new Error(
471-
"Could not extract the method name from the transaction envelope.",
472-
);
473-
}
474-
475-
const xdrContractId = Address.fromScAddress(
476-
invokeContractArgs.contractAddress(),
477-
).toString();
478-
479-
if (xdrContractId !== options.contractId) {
480-
throw new Error(
481-
`Transaction envelope targets contract ${xdrContractId}, but this Client is configured for ${options.contractId}.`,
482-
);
483-
}
489+
const invokeContractArgs = AssembledTransaction.validateInvokeContractOp(
490+
built,
491+
options.contractId,
492+
);
484493

485494
const method = invokeContractArgs.functionName().toString("utf-8");
486495
const txn = new AssembledTransaction({

test/unit/server/soroban/assembled_transaction.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,4 +307,39 @@ describe("Contract ID validation on deserialization", () => {
307307
`Transaction envelope targets contract ${attackerContractId}, but this Client is configured for ${victimContractId}.`,
308308
);
309309
});
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+
});
310345
});

0 commit comments

Comments
 (0)