Skip to content

[BUG] completeInputsByUdt Can Burn a Prefix-Matching Foreign UDT #515

Description

@phroi

Hello folks at CKB DevRel,

Udt.completeChangeToLock calls completeInputsByUdt, which can add and burn a foreign UDT cell whose type args extend the target args. This can destroy foreign tokens.

Minimal Reproduction

import { ccc } from "@ckb-ccc/core";

const client = new ccc.ClientPublicTestnet({ url: "https://example.invalid" });
const signer = new ccc.SignerCkbPrivateKey(client, `0x${"11".repeat(32)}`);
const lock = ccc.Script.from({
  codeHash: `0x${"1".repeat(64)}`,
  hashType: "type",
  args: "0x",
});
const type = ccc.Script.from({
  codeHash: `0x${"2".repeat(64)}`,
  hashType: "type",
  args: `0x${"aa".repeat(32)}`,
});
const udtCell = (id, args, balance) =>
  ccc.Cell.from({
    outPoint: { txHash: `0x${id.repeat(64)}`, index: 0 },
    cellOutput: {
      capacity: ccc.fixedPointFrom(142),
      lock,
      type: { codeHash: type.codeHash, hashType: type.hashType, args },
    },
    outputData: ccc.numLeToBytes(balance, 16),
  });
const realCell = udtCell("a", type.args, 100);
const foreignCell = udtCell("f", `${type.args}00`, 900);
signer.findCells = async function* ({ script }) {
  for (const cell of [realCell, foreignCell]) {
    const candidate = cell.cellOutput.type;
    if (
      candidate.codeHash === script.codeHash &&
      candidate.hashType === script.hashType &&
      candidate.args.startsWith(script.args)
    ) {
      yield cell;
    }
  }
};

const tx = ccc.Transaction.from({
  outputs: [{ lock, type }],
  outputsData: [ccc.numLeToBytes(50, 16)],
});
await tx.completeInputsByUdt(signer, type);
console.log(
  "foreign UDT input added without a matching output:",
  tx.inputs.some((input) => input.previousOutput.eq(foreignCell.outPoint)),
);
node repro.mjs

Behavior

foreign UDT input added without a matching output: true

completeInputsByUdt supplies { script: type, outputDataLenRange: [16, 0xffffffff] }; completeInputs calls Signer.findCells(filter, true), leaving order and limit unset. Signer.findCells makes the lock exact and nests the UDT filter. CKB's RPC filter has no script search mode, and its documented filter.script check is prefix-only; this is a footgun for any caller expecting exact nested script identity. completeInputs instead reads every returned cell as target balance, although existing inputs are checked with .eq(type).

Proposed Solution

Either constrain the query with scriptLenRange: [type.occupiedSize, type.occupiedSize + 1] after normalizing type with Script.from, or skip returned cells whose cellOutput.type is not .eq(type) before reading their balance or adding them as inputs.

Environment

Keep up the Great Work,
Phroi %54

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions