Skip to content

Refuse a token id a JSON number can no longer carry - #107

Merged
portdeveloper merged 1 commit into
portdeveloper:mainfrom
ColinkaMir:fix/refuse-unsafe-numeric-token-ids
Sep 25, 2026
Merged

portdeveloper merged 1 commit into
portdeveloper:mainfrom
ColinkaMir:fix/refuse-unsafe-numeric-token-ids

Conversation

@ColinkaMir

@ColinkaMir ColinkaMir commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Closes #106.

What changed

The precision is gone before any line of this repo runs. JSON.parse turns 9007199254740993 into 9007199254740992, so parseAction never sees the id the model wrote, String() then makes it an ordinary decimal, and requireTokenId from #105 accepts it because it is a legal uint256. The transfer that comes out is complete, signable, and for a different token.

Nothing downstream can recover the digit, so this refuses rather than repairs, at the two places a number can enter:

  • The action boundary (runAction, transfer_nft): the check sits before the String(a.tokenId) coercion. After it the value is indistinguishable from an id somebody meant, so the ordering is the fix rather than a detail of it.
  • requireTokenId: only the number branch changes. Strings and bigints keep the whole uint256 range, so 9007199254740993 stays a usable id when written as a string, which is exactly what the refusal tells the operator to do. 0 is still an id and hex still works.

runAction gained a transferNft seam beside the sendToken one it already had. Without it the ordering is unobservable: a test can see a refusal but not whether the wallet was reached first. Production callers pass neither, so the executed path is unchanged.

Verification

  • The model output verbatim from the issue, asserting the refusal and that a stubbed wallet recorded no call at all.
  • The same number arriving as a native tool argument, which reaches runAction as a real JS number rather than through parseAction: same boundary, same refusal, wallet still untouched.
  • The safe-integer boundary from both sides: 9007199254740991 accepted as a number, the next value refused as a number and accepted as a string.
  • The exported helpers refusing the same number, including transferNft before it needs a session, which is reachable only because Refuse an unreadable NFT token id instead of throwing or encoding zero #105 moved the id check ahead of the session check.
  • An exact large string decoded back out of the calldata unchanged, and uint256 max still legal, so the guard did not narrow the range.
  • Red test: with the sources checked out at the previous commit and these tests kept, six of the nine fail. Three are the defect itself (no refusal at three boundaries); the other three fail only because the transferNft seam does not exist yet, and I would rather say so than count them as evidence.

npm test: 524 pass. Three tests in test/native-tools-cli-exit.test.mjs fail in my environment, identically with and without this diff, and CI is green on master, so they are my local Node 20 against your Node 22 rather than anything from this change.

Scope

Nothing outside NFT token-id precision: no ABI change, no transaction-flow change, and no touching other numeric fields on other actions even where the same shape exists.

@ColinkaMir

Copy link
Copy Markdown
Contributor Author

Re-checked the diff against the scope before you spend time on it, and one line of it was thinner than the issue asks.

"Cover raw JSON input above the safe-integer boundary and a native-tool numeric argument": I had the JSON path and the exported helpers, but nothing asserting the action boundary when a real JS number arrives directly, which is the shape a native tool call takes. Added that test and force-pushed; nothing else changed.

Red test redone properly as well. My first run of it was wrong: the fix was already committed, so stashing removed only the test and the suite passed for the wrong reason. Checking the sources out at the previous commit while keeping the tests gives the honest number: six of the nine fail, four at the action boundary and two at the helpers. Three of those six are the missing refusal itself; the other three fail because the transferNft seam does not exist yet, and I would rather say that than count them as evidence.

Everything else stands: 524 pass locally, the three failures in native-tools-cli-exit.test.mjs are identical with and without this diff, and CI here is green on ubuntu and windows.

@ColinkaMir
ColinkaMir force-pushed the fix/refuse-unsafe-numeric-token-ids branch from 1764353 to 90d43fc Compare September 24, 2026 18:46
The precision is gone before any line of this repo runs. JSON.parse turns
9007199254740993 into 9007199254740992, so parseAction never sees the id the
model wrote, String() then makes it an ordinary decimal, and requireTokenId from
portdeveloper#105 accepts it because it is a legal uint256. The transfer is complete,
signable, and for a different token.

Nothing downstream can recover the digit, so this refuses rather than repairs,
at the two places a number can enter. At the action boundary the check sits
before the String() coercion, because after it the value is indistinguishable
from an id somebody meant. In requireTokenId only the number branch changes:
strings and bigints keep the whole uint256 range, so 9007199254740993 remains a
usable id when it is written as a string, which is what the refusal tells the
operator to do.

runAction gained a transferNft seam beside the sendToken one it already had.
Without it the ordering is unobservable: a test can see a refusal but not
whether the wallet was reached first, and ordering is the entire fix here.

Tests: the model output from the issue and the same number arriving as a native
tool argument, both asserting the refusal and that the stubbed wallet recorded
no call; the safe-integer boundary accepted as a number and the next value
refused; the same id accepted as a string; the exported helpers refusing the
number; an exact large string decoded back out of the calldata unchanged; and
uint256 max still legal. Six of the nine fail with the sources checked out at
the previous commit.
@ColinkaMir
ColinkaMir force-pushed the fix/refuse-unsafe-numeric-token-ids branch from 90d43fc to 7568045 Compare September 24, 2026 19:02

@portdeveloper portdeveloper left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, thanks

@portdeveloper
portdeveloper merged commit 1cac18f into portdeveloper:main Sep 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refuse unsafe numeric NFT token IDs before string coercion

2 participants