Skip to content

feat(sdk-coin-stx): accept a configured PoX-5 staking contract address - #10058

Closed
Doddanna17 wants to merge 1 commit into
masterfrom
SI-2089-bitgojs-pox5-configured-address
Closed

Doddanna17 wants to merge 1 commit into
masterfrom
SI-2089-bitgojs-pox5-configured-address

Conversation

@Doddanna17

Copy link
Copy Markdown
Contributor

Context

The Stacks Foundation PoX-5 test contract is deployed with the standard name pox-5 at a non-canonical boot address (SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY). The SDK's Utils.isValidContractAddress is address-only, so any parse of a register targeting that address is rejected unless the caller supplies allowedStakingContracts.

That forces every consumer to pass the allowlist — including the offline vault, which only rehydrates a tx to explain/sign and would otherwise need its own change and a release.

Change

Let the address come from the network (statics) instead, so no caller needs to pass anything:

File Change
modules/statics/src/networks.ts StacksNetwork.allowedStakingContractAddresses?: ReadonlyArray<string>; mainnet Stx sets ['SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY']
modules/sdk-coin-stx/src/lib/utils.ts isValidContractAddress also accepts a configured address
modules/sdk-coin-stx/src/lib/pox5Builder.ts isValidContractCall accepts the canonical or a configured address (builder parity)

The caller-supplied allowlist (PR #10041) is kept unchanged — it remains a working override.

Why this shape

  • No caller changes: a tx targeting the configured address parses with no options at all, so the vault (and any other consumer) works unmodified.
  • Default-safe: the field is set only on mainnet; testnet/signet are untouched, so their behaviour is byte-identical.
  • Opt-in by construction: only an address explicitly listed in statics is accepted; unknown addresses are still rejected.

Trade-off (for reviewers)

This puts the allowance in a shared package, so it applies to every consumer of the Stx network rather than one deployment. It is the deliberate alternative to the caller-allowlist approach, which is deployment-scoped but requires each consumer (incl. the vault) to pass the pair. Chosen here so the vault needs no change and no release. Remove with the test contract.

Commits

SHA Subject
6542d8b4ef feat(sdk-coin-stx): accept a configured PoX-5 staking contract address

Tests

  • isValidContractAddress: accepts a configured address, still accepts canonical, rejects an unknown address, and testnet-with-no-config still rejects.
  • factory.from(hex) with NO allowlist rehydrates a register-for-bond targeting the configured address — the behaviour that lets the vault work unchanged.
  • Pox5Builder.isValidContractCall: true for configured + pox-5, false for an unknown address.
  • Existing caller-supplied-allowlist tests still pass.
cd modules/sdk-coin-stx && BITGOJS_TEST_PASSWORD=test yarn unit-test   # 343 passing
cd modules/statics      && BITGOJS_TEST_PASSWORD=test yarn unit-test   # 41174 passing

Related

  • SI-2089 (parent) · SI-2026 (vault) · SI-2098 (staking-service reads)
  • Alternative to BitGo/bitgo-microservices#63818 + BitGo/offline-vault-console#1329 (caller-allowlist, deployment-scoped)

The Stacks Foundation PoX-5 test contract uses the standard name `pox-5`
at a non-canonical boot address. `isValidContractAddress` is address-only,
so a register targeting it is rejected on every parse unless the caller
supplies an allowlist.

Allow the address to come from the network instead: add
`allowedStakingContractAddresses` to the StacksNetwork statics (set on
mainnet to the test address only) and accept it in both
`isValidContractAddress` and `Pox5Builder.isValidContractCall`. A consumer
can then parse and sign a configured address with no caller changes.

Default-safe: unset on testnet/signet, so behaviour there is unchanged.
The caller-supplied allowlist remains a working override.

SI-2089

TICKET: SI-2089
@linear-code

linear-code Bot commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

SI-2089

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Unit tests are failing on Node 26.x (Current release line, non-blocking). This is not an LTS version yet, so it does not block merge, but it signals an incompatibility to fix before Node 26.x becomes LTS.

View run

@Doddanna17
Doddanna17 marked this pull request as ready for review October 26, 2026 08:12
@Doddanna17
Doddanna17 requested a review from a team as a code owner October 26, 2026 08:12

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@Doddanna17

Copy link
Copy Markdown
Contributor Author

@claude review

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

addr === network.sendmanymemoContractAddress ||
addr === network.sbtcWithdrawalContractAddress
addr === network.sbtcWithdrawalContractAddress ||
(network.allowedStakingContractAddresses ?? []).includes(addr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Every mainnet consumer will now build and parse pox-4 and send-many-memo calls to the third-party test deployer, not just its pox-5. isValidContractAddress trusts the configured address on its own. ContractBuilder.isContractPairValid then accepts any canonical name at any address that passes. Fix: scope the trust to the (pox-5, configured address) pair, checking it in isContractPairValid and Pox5Builder.isValidContractCall, and leave isValidContractAddress address-only for the canonical contracts.

Why this was flagged

The new clause at utils.ts:295 adds SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY to every check done through isValidContractAddress. ContractBuilder.contractAddress (contractBuilder.ts:108) therefore accepts it. isContractPairValid (contractBuilder.ts:66-73) returns true when isCanonicalName && isCanonicalAddress, and canonical names include pox-4 and send-many-memo. So a send-many-memo or pox-4 call to that deployer builds, and through factory.from(hex) it is rehydrated and explained like a trusted BitGo contract. On base it was rejected with 'Invalid contract address'. That deployer is an outside Stacks Foundation test key and can deploy a contract under any name, such as a send-many-memo that sends funds elsewhere. Explain/sign paths would show such a call as a normal send-many or stacking call. The doc comment's own @ obligation note says the address is trusted for every Stacks transaction. The PR describes the change as pox-5 only, and the test edit in contractBuilder.ts (swapping in a new dummy address) shows that cross-name pairing now happens.

Verification: On mainnet, a build or factory.from(hex) parse of a contract call whose deployer is SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY and whose name is pox-4 or send-many-memo passes. contractBuilder.ts:66-73 isContractPairValid returns true whenever isCanonicalName && isCanonicalAddress. On the base commit, line 110 throws "Invalid contract address" for this deployer.

Comment on lines +1375 to +1378
// Temporary: the Stacks Foundation pox-5 test contract (standard name `pox-5`, non-canonical boot
// address). Allows transactions targeting it to parse/build without a caller-supplied allowlist.
// Remove with the test.
allowedStakingContractAddresses = ['SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY'];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Every mainnet Stx consumer will build, parse and route real-fund pox-5 staking calls to a third-party test deployer, with no opt-in and no expiry. The constant sits on the shared mainnet Stx network, so Pox5Builder.isValidContractCall and factory.from(hex) treat SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY.pox-5 as canonical pox-5 for every package. Fix: limit the trust to deployments that ask for it. For example, keep the network field empty by default and let a consumer such as the vault turn it on through config, or add a hard expiry such as a block-height or date check, while keeping the caller allowlist override.

Why this was flagged

The mainnet Stx class at networks.ts:1378 sets allowedStakingContractAddresses = ['SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY']. Every package that imports @ bitgo/statics gets this value, not only the staking service and the vault. Pox5Builder.isValidContractCall (pox5Builder.ts:174-176) now sends any tx whose payload targets that address with name pox-5 to the Pox5Builder. ContractBuilder.contractAddress accepts it through isValidContractAddress (utils.ts:295), so a mainnet register-for-bond or stake that locks user STX in a test contract builds, explains and signs as a normal pox-5 staking call. On the base branch that call is rejected with 'Invalid contract address' unless the caller passes allowedStakingContracts, which keeps the trust deployment-scoped. The PR description calls this an accepted trade-off. That note does not bound the exposure: nothing in the code ties the entry to an expiry, environment or caller.

Verification: Trigger: any mainnet consumer of @ bitgo/statics builds or parses a contract call whose deployer is SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY. networks.ts:1378 puts the address on the shared mainnet Stx class. utils.isValidContractAddress (utils.ts:295) accepts the address for any contract name. It is then accepted where the base rejected it with 'Invalid contract address'.

@Doddanna17

Copy link
Copy Markdown
Contributor Author

Superseded by #10062 — closing.

This PR extended isValidContractAddress with the test address, which (because isContractPairValid accepts any canonical name at any canonical address) made pox-4 and send-many-memo at that address acceptable for every mainnet consumer, and the allowance carried no opt-in or expiry — the two 🔴 findings from the review.

#10062 replaces it with a pair-scoped, self-expiring allowance:

  • StacksNetwork.allowedStakingContracts?: { name; address; expiresAt? }[] on the statics, seeded on mainnet with { name: 'pox-5', address: 'SP3JVSE…', expiresAt: '2026-11-02' }
  • consulted in isContractPairValid and Pox5Builder.isValidContractCall
  • isValidContractAddress stays address-only, so pox-4 / send-many-memo at that address are rejected again
  • past the expiry the entry is ignored and the SDK fails closed with Invalid contract address

→ #10062

@Doddanna17 Doddanna17 closed this Oct 26, 2026
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.

1 participant