Summary
The untrusted redeem path in SubscriptionModule._redeemUntrusted transfers the subscriber's own personal CRC directly to the recipient (no trust path / no pathfinder needed). It reverts with ExecutionFailed() (0xacfdb444) when the subscriber's Safe holds no personal CRC.
In the current Circles model many subscribers are minting humans who never claim their issuance, so their personal-CRC balance is ~0 even though they have a large unclaimed mint backlog. Proposal: call personalMint() on the Hub (via the subscriber's Safe) immediately before the transfer, materializing the accrued personal CRC so the direct transfer can settle.
Background / root cause
The subindexer redeem job has failed daily for ~a week. The batch bundles all due subscriptions into one EIP-7702 multisend (all-or-nothing), so a single failing sub reverts the whole batch. Simulating each sub individually (eth_call) isolated the failure:
| category |
subscriber |
result |
| trusted |
0xcf6dc1… |
✅ would succeed |
| untrusted |
0xede0c2… |
❌ ExecutionFailed() |
| trusted |
0x6b6968… |
✅ would succeed |
| trusted |
0x6b6968… |
✅ would succeed |
The trusted subs succeed even with ~0 personal balance because operateFlowMatrix sources value from other/held CRC along the trust graph. The untrusted sub fails because _redeemUntrusted hard-transfers _toTokenId(subscriber) (personal CRC), which the subscriber doesn't hold as balance.
The three trusted ones were redeemed manually one-at-a-time to clear the backlog:
0x86d33529fd75f2ba91573fa4e6566e3ddc821e72d2dbf0600c581566cb0838d8
0x084eed6ed631ebb1810e5577654130057358592d87090a902167cd2dfe33d895
0x280df6c8695d4c63d650e81423c052ec2e7ce5d42c3787b7f009b681fe1fe08f
The untrusted one remains stuck.
Evidence that auto-mint fixes it
On-chain checks for the stuck untrusted subscriber 0xede0c2e70e8e2d54609c1bdf79595506b6f623fe (Hub 0xc12C1E50ABB450d6205Ea2C3Fa861b3B834d13e8):
isHuman(subscriber) → true
calculateIssuance(subscriber) → ~80.97 CRC claimable right now
- redeemable amount needed → 3.93 CRC
So the value exists as unclaimed issuance, not balance. personalMint() converts it to spendable personal CRC, after which the existing direct transfer settles. Confirmed Hub safeTransferFrom does not require the receiver to trust the token, so the direct transfer always lands.
Proposed change
In _redeemUntrusted, claim issuance before the transfer. Best-effort — execTransactionFromModule swallows reverts, so non-minting avatars simply no-op:
function _redeemUntrusted(bytes32 id, Subscription memory sub) internal {
uint256 amount = LibTransient.tUint256(T_REDEEMABLE_AMOUNT).get();
// Materialize accrued personal CRC so the subscriber can pay.
// Best-effort: only registered humans mint; a revert here is ignored.
ISafe(sub.subscriber).execTransactionFromModule(
HUB, 0, abi.encodeCall(IHubV2.personalMint, ()), Enum.Operation.Call
);
require(
ISafe(sub.subscriber).execTransactionFromModule(
HUB, 0,
abi.encodeCall(ERC1155.safeTransferFrom,
(sub.subscriber, sub.recipient, _toTokenId(sub.subscriber), amount, "")),
Enum.Operation.Call
),
Errors.ExecutionFailed()
);
emit Redeemed(id, sub.subscriber, sub.recipient, sub.lastRedeemed + sub.frequency);
LibTransient.tUint256(T_REDEEMABLE_AMOUNT).clear();
}
- Add
function personalMint() external; to the IHubV2 interface.
- Optional: hoist the best-effort
personalMint to the top of redeem() so trusted/group flows also benefit from freshly-claimed collateral (harmless since best-effort).
Caveats / edge cases
- 14-day accrual ceiling (~336 CRC).
personalMint only claims up to 14 days; older issuance is forfeited. A backlog larger than what's claimable still reverts. Mitigate by redeeming regularly (and by hardening the batch so under-funded subs don't block the job).
- Net draw must stay ≤ 1 CRC/hr (mint rate = 24 CRC/day) for indefinite sustainability. The current sub draws ~0.005 CRC/hr, so mint comfortably keeps up.
- Non-human subscribers can't mint. Groups/orgs with no personal CRC still fail this path. Complementary fix: generalize the transfer to
safeBatchTransferFrom of whatever CRC the subscriber holds (ids/values passed in data, summing to the redeemable amount) — direct transfers need no trust either. Auto-mint covers all human subscribers; batch-transfer covers the rest.
Tasks
References
- Hub
personalMint: 1 CRC/hr (24/day), 14-day max claim window, humans only, demurraged.
- Hub direct
safeTransferFrom/safeBatchTransferFrom: no receiver-trust check.
operateFlowMatrix can source group/collateral CRC the sender holds (why the trusted path works at ~0 personal balance).
Summary
The
untrustedredeem path inSubscriptionModule._redeemUntrustedtransfers the subscriber's own personal CRC directly to the recipient (no trust path / no pathfinder needed). It reverts withExecutionFailed()(0xacfdb444) when the subscriber's Safe holds no personal CRC.In the current Circles model many subscribers are minting humans who never claim their issuance, so their personal-CRC balance is ~0 even though they have a large unclaimed mint backlog. Proposal: call
personalMint()on the Hub (via the subscriber's Safe) immediately before the transfer, materializing the accrued personal CRC so the direct transfer can settle.Background / root cause
The subindexer redeem job has failed daily for ~a week. The batch bundles all due subscriptions into one EIP-7702 multisend (all-or-nothing), so a single failing sub reverts the whole batch. Simulating each sub individually (
eth_call) isolated the failure:0xcf6dc1…0xede0c2…ExecutionFailed()0x6b6968…0x6b6968…The trusted subs succeed even with ~0 personal balance because
operateFlowMatrixsources value from other/held CRC along the trust graph. The untrusted sub fails because_redeemUntrustedhard-transfers_toTokenId(subscriber)(personal CRC), which the subscriber doesn't hold as balance.The three trusted ones were redeemed manually one-at-a-time to clear the backlog:
0x86d33529fd75f2ba91573fa4e6566e3ddc821e72d2dbf0600c581566cb0838d80x084eed6ed631ebb1810e5577654130057358592d87090a902167cd2dfe33d8950x280df6c8695d4c63d650e81423c052ec2e7ce5d42c3787b7f009b681fe1fe08fThe untrusted one remains stuck.
Evidence that auto-mint fixes it
On-chain checks for the stuck untrusted subscriber
0xede0c2e70e8e2d54609c1bdf79595506b6f623fe(Hub0xc12C1E50ABB450d6205Ea2C3Fa861b3B834d13e8):isHuman(subscriber)→ truecalculateIssuance(subscriber)→ ~80.97 CRC claimable right nowSo the value exists as unclaimed issuance, not balance.
personalMint()converts it to spendable personal CRC, after which the existing direct transfer settles. Confirmed HubsafeTransferFromdoes not require the receiver to trust the token, so the direct transfer always lands.Proposed change
In
_redeemUntrusted, claim issuance before the transfer. Best-effort —execTransactionFromModuleswallows reverts, so non-minting avatars simply no-op:function personalMint() external;to theIHubV2interface.personalMintto the top ofredeem()so trusted/group flows also benefit from freshly-claimed collateral (harmless since best-effort).Caveats / edge cases
personalMintonly claims up to 14 days; older issuance is forfeited. A backlog larger than what's claimable still reverts. Mitigate by redeeming regularly (and by hardening the batch so under-funded subs don't block the job).safeBatchTransferFromof whatever CRC the subscriber holds (ids/values passed indata, summing to the redeemable amount) — direct transfers need no trust either. Auto-mint covers all human subscribers; batch-transfer covers the rest.Tasks
personalMint()toIHubV2.personalMintcall to_redeemUntrusted(or toredeem()).safeBatchTransferFromfallback for non-human / no-personal-CRC subscribers.subindexer) harden the batch to simulate-then-skip so one failing sub can't block the rest.References
personalMint: 1 CRC/hr (24/day), 14-day max claim window, humans only, demurraged.safeTransferFrom/safeBatchTransferFrom: no receiver-trust check.operateFlowMatrixcan source group/collateral CRC the sender holds (why the trusted path works at ~0 personal balance).