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.ts → lib/entry-transfer.ts).
Returned flow matrix (decoded)
mig.txs[0].data decodes (selector 0x0d22d9b5) to:
flowVertices:
[0] 0x6B69683C8897e3d18e74B1Ba117b49f80423Da5d — avatar
[1] 0x93eD5A96347927ff6fF6b790F8Cf5258240c321f — group
[2] 0xd4cf9afd3ae777c24454b70dd28e32d1bd516f05 — CrcV2_RegisterOrganization, Hub.totalSupply == 0
[3] 0xe171a76de6b645a28b3767f84b177a4f6659a3d7 — CrcV2_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
- 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.
- 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.
Summary
PermissionlessGroup.migration({ avatar, amount })returned a flow matrix whoseoperateFlowMatrixcalldata references twoCrcV2_RegisterOrganizationvertices withtotalSupply == 0. The resulting tx reverts deterministically in Hub V2's ERC1155 transfer with an empty0x, 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)https://rpc.staging.aboutcircles.com/score-groupshttps://rpc.staging.aboutcircles.com/0xc12C1E50ABB450d6205Ea2C3Fa861b3B834d13e8(Gnosis Chain, chainId 100)0x5F99a795dD2743C36D63511f0D4bc667e6d3cDB52059)Repro
Consumer that surfaced the bug: https://github.com/pboes/daily-chess-puzzle (
hooks/use-entry.ts→lib/entry-transfer.ts).Returned flow matrix (decoded)
mig.txs[0].datadecodes (selector0x0d22d9b5) to:flowVertices:[0]0x6B69683C8897e3d18e74B1Ba117b49f80423Da5d— avatar[1]0x93eD5A96347927ff6fF6b790F8Cf5258240c321f— group[2]0xd4cf9afd3ae777c24454b70dd28e32d1bd516f05—CrcV2_RegisterOrganization,Hub.totalSupply == 0[3]0xe171a76de6b645a28b3767f84b177a4f6659a3d7—CrcV2_RegisterOrganization,Hub.totalSupply == 0flow(3 edges, all amount0x2c8a816f6b830000=200595000000000000)streams:[ { sourceCoordinate: 0, flowEdgeIds: [2], data: 0x } ]packedCoordinates((tokenOwner, sender, receiver)per edge, per Hub V2's_verifyFlowMatrix):tokenOwner=v[3]=0xe171…,sender=v[0]=avatar,receiver=v[3]=0xe171…tokenOwner=v[1]=group,sender=v[1]=group,receiver=v[1]=grouptokenOwner=v[2]=0xd4cf…,sender=v[0]=avatar,receiver=v[0]=avataredge[0] asks the avatar to transfer
0.200595of0xe171…'s personal CRC, but:Same for
0xd4cf…. Hub V2_safeTransferFromreverts with a barerevert()on insufficient balance, surfacing as empty0x. Forge trace:(Reverts very early, consistent with the first edge's pre-transfer balance check.)
Why this matters
Any client that calls
group.migrationand thensendTransactions(mig.txs)will fail userOp simulation with an opaque0x. Players in the chess miniapp see "UserOperation reverted during simulation with reason: 0x" and have no path to recover — they can't directlyHub.groupMinteither because the score policy at0x450D…9569rejects personal-CRC collateral (InvalidCollateralForPersonalIssuanceMint()), andSCORE_GROUPhas no directTrustRelationsrows.Suggested fixes
tokenOwneris an org / has zerototalSupply. If the only available collateral is org tokens, fail the request rather than returning an unusable path.PermissionlessGroup.migrationcould validate the decodedoperateFlowMatrixbefore returning — for each edge withsender != tokenOwner, assertHub.balanceOf(sender, tokenId(tokenOwner)) >= amount. Throw a typed error (InvalidMigrationPathor 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
0xe29d…, 1395 CRC of0xe838…, etc.) and several of those issuers are trusted bySCORE_GROUP(Hub.isTrustedreturnstrue), so a valid path exists — the solver just didn't pick it.