Repository navigation
feat(sdk-coin-stx): accept a configured PoX-5 staking contract address - #10058
Doddanna17 wants to merge 1 commit into
Conversation
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
|
|
There was a problem hiding this comment.
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.
|
@claude review |
|
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) |
There was a problem hiding this comment.
🔴 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.
| // 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']; |
There was a problem hiding this comment.
🔴 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'.
|
Superseded by #10062 — closing. This PR extended #10062 replaces it with a pair-scoped, self-expiring allowance:
→ #10062 |
Context
The Stacks Foundation PoX-5 test contract is deployed with the standard name
pox-5at a non-canonical boot address (SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY). The SDK'sUtils.isValidContractAddressis address-only, so any parse of a register targeting that address is rejected unless the caller suppliesallowedStakingContracts.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:
modules/statics/src/networks.tsStacksNetwork.allowedStakingContractAddresses?: ReadonlyArray<string>; mainnetStxsets['SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY']modules/sdk-coin-stx/src/lib/utils.tsisValidContractAddressalso accepts a configured addressmodules/sdk-coin-stx/src/lib/pox5Builder.tsisValidContractCallaccepts 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
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
6542d8b4efTests
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 aregister-for-bondtargeting the configured address — the behaviour that lets the vault work unchanged.Pox5Builder.isValidContractCall: true for configured +pox-5, false for an unknown address.Related