Skip to content

Automatic claims#157

Open
exo404 wants to merge 4 commits into
devfrom
141-automatic-claiming
Open

Automatic claims#157
exo404 wants to merge 4 commits into
devfrom
141-automatic-claiming

Conversation

@exo404
Copy link
Copy Markdown
Contributor

@exo404 exo404 commented Apr 13, 2026

Summary

This PR adds Gelato-powered automatic claims to the existing AutomaticSavingCircles automation extension.

Gelato can now resolve and execute claimable payouts automatically once both required conditions are true:

  • all members have deposited for the payout round,
  • the payout round’s time window has passed.

What Changed

Added a new automatic claim flow:

  • claimChecker() scans all circles and returns executable calldata when any member is eligible to claim.
  • getEligibleAutomatedClaims() returns all eligible (circleId, member) claim targets.
  • batchExecuteAutomatedClaims(...) executes claim targets through Gelato’s configured executor.
  • executeAutomatedClaimTarget(...) is used internally so failed targets do not block the full batch with the try/catch pattern.
  • AutomatedClaimFailed(...) is emitted when an individual claim target fails during batch execution.

Claim eligibility requires:

  • the circle is active,
  • the circle is not decommissionable,
  • the member’s payout round time has passed,
  • SavingCircles.isMemberWithdrawable(circleId, member) returns true.

This means Gelato only claims when deposits are complete and the time condition is satisfied.

SavingCircles Change

withdrawFor(...) is now truly permissionless for valid claim recipients. The caller no longer needs to be a circle member; instead, the target member must be a valid member and must be withdrawable.

This is required because the automation contract is not a member of each circle, but it needs to trigger claims on behalf of eligible members. The payout safety checks still remain in place through _claimable(...).

Tests

Added coverage for:

  • claim targets are not returned before the round time has passed,
  • claim targets are not returned when deposits are incomplete,
  • claimChecker() returns the correct batch execution payload,
  • automatic claims execute across multiple circles,
  • only the configured Gelato executor can run claim batches,
  • mismatched claim batch arrays revert,
  • non-member callers can trigger withdrawFor(...) for valid recipients,
  • invalid claim recipients still revert.

Validation:

  • forge test
    Full suite passes: 156 tests.

@exo404 exo404 requested a review from bagelface April 13, 2026 13:40
@exo404 exo404 self-assigned this Apr 13, 2026
@exo404 exo404 added the enhancement New feature or request label Apr 13, 2026
@exo404 exo404 linked an issue Apr 13, 2026 that may be closed by this pull request

/**
* @notice Gelato resolver-style checker for automatic claims across every circle
* @return canExec Whether Gelato should execute the claims
Copy link
Copy Markdown
Collaborator

@bagelface bagelface Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a discrepancy here. need to either update this comment to "Whether Gelato can execute the claims" or change the name of the variable.

(bool success,) = address(automaticSavingCircles).call(execPayload);
assertTrue(success);

assertTrue(savingCircles.hasClaimed(baseCircleId, alice));
Copy link
Copy Markdown
Collaborator

@bagelface bagelface Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check these values are false and balance is correct before the automated claim? maybe not needed since we dont seem to be checking pre-state for other tests

* @param _index Current write index in the output arrays
* @return nextIndex Updated write index after appending any eligible targets
*/
function _populateEligibleAutomatedClaimsForCircle(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming of this function is a bit confusing, since it implies it's populating an array but really it's just returning the next index. The comments and function name prescribe a use to the function, which I think isn't ideal. Consider renaming to something like "_getNextEligibleAutomationClaimIndexForCircle"

Copy link
Copy Markdown
Collaborator

@bagelface bagelface left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and every comment in this thread was generated by Claude 🤖

Overall the PR is well-structured and consistent with the existing deposit automation pattern. A few issues below, not covered by existing comments.

revert ISavingCircles.NotWithdrawable();
}

SAVING_CIRCLES.withdrawFor(_circleId, _member);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_executeAutomatedClaimTarget fetches getCircleMembers and calls _getMemberIndex to check membership, then calls _isEligibleForAutomatedClaim, which calls isMemberWithdrawable → _claimable → _activeClaimableCheck → _getMemberIndex — so membership is verified twice with two separate getCircleMembers calls. Since withdrawFor → _withdraw has onlyMember(_id, _member) as a hard guard, the explicit NotMember revert at line 239 is also redundant. Consider dropping the pre-check and relying on withdrawFor to enforce it, or at least share the member list with _isEligibleForAutomatedClaim to avoid the duplicate fetch.

if (!SAVING_CIRCLES.isActive(_circleId)) return false;
if (_circle.effectiveCircleStartTime == 0) return false;
if (SAVING_CIRCLES.isDecommissionable(_circleId)) return false;
if (block.timestamp < _roundEndTime(_circle, _memberIndex)) return false;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes _memberIndex (the position in getCircleMembers()) equals the member's payout round index. That invariant holds as long as member order in the array is the canonical payout order, but it is implicit. Worth adding a comment asserting this assumption so a future refactor of how member order is stored does not silently break claim eligibility.

Copy link
Copy Markdown
Collaborator

@bagelface bagelface Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we make this assumption throughout the contract, so can probably skip this

function _countEligibleAutomatedClaimsForCircle(uint256 _circleId) internal view returns (uint256 eligibleCount) {
try SAVING_CIRCLES.getCircle(_circleId) returns (ISavingCircles.Circle memory _circle) {
if (!SAVING_CIRCLES.isActive(_circleId)) return 0;
if (SAVING_CIRCLES.isDecommissionable(_circleId)) return 0;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isActive and isDecommissionable are checked here (and again in _populateEligibleAutomatedClaimsForCircle) before calling _isEligibleForAutomatedClaim, which already checks both conditions (lines 438–440). The early-exit guards are redundant and could diverge from the authoritative check over time. Consider removing them from _countEligibleAutomatedClaimsForCircle and _populateEligibleAutomatedClaimsForCircle and letting _isEligibleForAutomatedClaim be the single gate.

Copy link
Copy Markdown
Collaborator

@bagelface bagelface Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally agree with this comment, but assuming the early-exit guards aren't too expensive it can be worth leaving in just to future proof. isDecommissionable loops over members, so this is sort of a heavy check.

members = new address[](0);
}

execPayload = abi.encodeCall(IAutomaticSavingCircles.batchExecuteAutomatedClaims, (circleIds, members));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When automationExecutor == address(0), canExec is false but execPayload is still a valid (empty-arrays) encoded call. This is safe (the call would revert on onlyAutomationExecutor), but Gelato's own resolver examples use execPayload = "" when canExec = false. Low priority, but worth aligning for consistency with Gelato conventions.


function _fundEnableAndApprove(address _member, uint256 _amount) internal {
token.mint(_member, _amount);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing parity with the deposit side: there is no test that a failed individual claim target (e.g. already claimed, or round time not yet passed) emits AutomatedClaimFailed and does not block the rest of the batch. Worth adding to match the coverage pattern for deposits.

Comment thread src/contracts/AutomaticSavingCircles.sol
Copy link
Copy Markdown
Collaborator

@bagelface bagelface left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got a few comments on here that I think are worth addressing.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Automatic Claiming

2 participants