Skip to content

Auto-mint personal CRC in _redeemUntrusted so subscribers with unclaimed issuance can redeem #29

Description

@bh2smith

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

  1. 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).
  2. 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.
  3. 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

  • Add personalMint() to IHubV2.
  • Add best-effort personalMint call to _redeemUntrusted (or to redeem()).
  • Tests: untrusted redeem succeeds for a human with unclaimed issuance and ~0 balance; remains a no-op-safe revert for a non-human with no personal CRC.
  • (Stretch) safeBatchTransferFrom fallback for non-human / no-personal-CRC subscribers.
  • (Relayer, separate / subindexer) harden the batch to simulate-then-skip so one failing sub can't block the rest.

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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions