Skip to content

permissionless-groups: group.migration returns flow matrix referencing zero-supply org tokens (reverts on simulation) #54

Description

@bh2smith

Summary

PermissionlessGroup.migration({ avatar, amount }) returned a flow matrix whose operateFlowMatrix calldata references two CrcV2_RegisterOrganization vertices with totalSupply == 0. The resulting tx reverts deterministically in Hub V2's ERC1155 transfer with an empty 0x, so any consumer that signs the returned txs fails at userOp simulation.

Environment

  • @aboutcircles/sdk-permissionless-groups@0.1.43 (also @aboutcircles/sdk@0.1.43, @aboutcircles/sdk-utils@0.1.43)
  • Backend: https://rpc.staging.aboutcircles.com/score-groups
  • Pathfinder / Circles RPC: https://rpc.staging.aboutcircles.com/
  • Hub V2: 0xc12C1E50ABB450d6205Ea2C3Fa861b3B834d13e8 (Gnosis Chain, chainId 100)
  • LiftERC20: 0x5F99a795dD2743C36D63511f0D4bc667e6d3cDB5
  • Observed: 2026-06-05 UTC (Hub day 2059)

Repro

const group = new PermissionlessGroup({
  groupAddress: "0x93eD5A96347927ff6fF6b790F8Cf5258240c321f", // score-gated group
  hubAddress:   "0xc12C1E50ABB450d6205Ea2C3Fa861b3B834d13e8",
  liftERC20Address: "0x5F99a795dD2743C36D63511f0D4bc667e6d3cDB5",
  backendBaseUrl: "https://rpc.staging.aboutcircles.com/score-groups",
  rpcUrl:         "https://rpc.staging.aboutcircles.com/",
  circlesConfig,
});

const avatar = "0x6B69683C8897e3d18e74B1Ba117b49f80423Da5d";
const mig = await group.migration({ avatar, amount: 200595000000000000n }); // ~0.2 group-CRC
// mig.txs[0] is operateFlowMatrix on Hub V2; simulating it from `avatar` reverts with 0x.

Consumer that surfaced the bug: https://github.com/pboes/daily-chess-puzzle (hooks/use-entry.tslib/entry-transfer.ts).

Returned flow matrix (decoded)

mig.txs[0].data decodes (selector 0x0d22d9b5) to:

  • flowVertices:
    • [0] 0x6B69683C8897e3d18e74B1Ba117b49f80423Da5d — avatar
    • [1] 0x93eD5A96347927ff6fF6b790F8Cf5258240c321f — group
    • [2] 0xd4cf9afd3ae777c24454b70dd28e32d1bd516f05CrcV2_RegisterOrganization, Hub.totalSupply == 0
    • [3] 0xe171a76de6b645a28b3767f84b177a4f6659a3d7CrcV2_RegisterOrganization, Hub.totalSupply == 0
  • flow (3 edges, all amount 0x2c8a816f6b830000 = 200595000000000000)
  • streams: [ { sourceCoordinate: 0, flowEdgeIds: [2], data: 0x } ]
  • packedCoordinates ((tokenOwner, sender, receiver) per edge, per Hub V2's _verifyFlowMatrix):
    • edge[0]: tokenOwner=v[3]=0xe171…, sender=v[0]=avatar, receiver=v[3]=0xe171…
    • edge[1]: tokenOwner=v[1]=group, sender=v[1]=group, receiver=v[1]=group
    • edge[2]: tokenOwner=v[2]=0xd4cf…, sender=v[0]=avatar, receiver=v[0]=avatar

edge[0] asks the avatar to transfer 0.200595 of 0xe171…'s personal CRC, but:

$ cast call $HUB "balanceOf(address,uint256)" $AVATAR $(cast --to-uint256 0xe171a76de6b645a28b3767f84b177a4f6659a3d7) --rpc-url https://rpc.aboutcircles.com
0
$ cast call $HUB "totalSupply(uint256)" $(cast --to-uint256 0xe171a76de6b645a28b3767f84b177a4f6659a3d7) --rpc-url https://rpc.aboutcircles.com
0

Same for 0xd4cf…. Hub V2 _safeTransferFrom reverts with a bare revert() on insufficient balance, surfacing as empty 0x. Forge trace:

[478] 0xc12C1E50ABB450d6205Ea2C3Fa861b3B834d13e8::operateFlowMatrix()
  └─ ← [Revert] EvmError: Revert
Gas used: 33170

(Reverts very early, consistent with the first edge's pre-transfer balance check.)

Why this matters

Any client that calls group.migration and then sendTransactions(mig.txs) will fail userOp simulation with an opaque 0x. Players in the chess miniapp see "UserOperation reverted during simulation with reason: 0x" and have no path to recover — they can't directly Hub.groupMint either because the score policy at 0x450D…9569 rejects personal-CRC collateral (InvalidCollateralForPersonalIssuanceMint()), and SCORE_GROUP has no direct TrustRelations rows.

Suggested fixes

  1. Backend (root cause): the score-groups path solver shouldn't emit edges whose tokenOwner is an org / has zero totalSupply. If the only available collateral is org tokens, fail the request rather than returning an unusable path.
  2. SDK defense in depth: PermissionlessGroup.migration could validate the decoded operateFlowMatrix before returning — for each edge with sender != tokenOwner, assert Hub.balanceOf(sender, tokenId(tokenOwner)) >= amount. Throw a typed error (InvalidMigrationPath or similar) so consumers can surface "retry later" instead of forwarding the doomed tx for signature.

Happy to PR (2) if helpful — let me know your preference.

Notes

  • The avatar genuinely has migratable backing (1408 CRC of 0xe29d…, 1395 CRC of 0xe838…, etc.) and several of those issuers are trusted by SCORE_GROUP (Hub.isTrusted returns true), so a valid path exists — the solver just didn't pick it.
  • Yesterday (2026-06-04) the same call from the same avatar worked, so this is intermittent / state-dependent rather than a hard config bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions