Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 3 additions & 21 deletions src/contracts/AutomaticSavingCircles.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
* @param _savingCircles Address of the main SavingCircles contract
* @param _owner Owner of the automatic deposits extension
*/
constructor(address _savingCircles, address _owner) Ownable(_owner) {

Check warning on line 52 in src/contracts/AutomaticSavingCircles.sol

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

Variable "_owner" is unused
SAVING_CIRCLES = ISavingCircles(_savingCircles);
}

Expand Down Expand Up @@ -259,10 +259,9 @@
// Check opt-in before fetching members while retaining the specific error for an invalid circle.
if (!automaticClaimsEnabled[_circleId][_member]) revert AutomaticClaimsDisabled();

address[] memory members = SAVING_CIRCLES.getCircleMembers(_circleId);
(bool isMember, uint256 memberIndex) = _getMemberIndex(members, _member);

if (!isMember) revert ISavingCircles.NotMember();
// A non-member resolves to index 0 here, but `isMemberWithdrawable` inside the eligibility check
// rejects them, so no separate membership lookup is needed.
uint256 memberIndex = SAVING_CIRCLES.memberIndex(_circleId, _member);
if (!_isEligibleForAutomatedClaim(_circleId, _circle, _member, memberIndex)) {
revert ISavingCircles.NotWithdrawable();
}
Expand Down Expand Up @@ -326,7 +325,7 @@
* @param _currentBalance Amount already deposited by the member in the active round
* @return Whether the member can be included in the automation payload
*/
function _isEligibleForAutomatedDeposit(

Check warning on line 328 in src/contracts/AutomaticSavingCircles.sol

View workflow job for this annotation

GitHub Actions / Lint Commit Messages

Function order is incorrect, internal view function can not go after internal pure function (line 316)
uint256 _circleId,
ISavingCircles.Circle memory _circle,
address _member,
Expand Down Expand Up @@ -512,21 +511,4 @@
return (true, _balances[i]);
}
}

/**
* @dev Looks up whether a member belongs to the supplied member snapshot and returns their index
* @param _members Snapshot of circle members
* @param _member Member being searched for
* @return isMember Whether the member was found in the snapshot
* @return memberIndex The member's zero-based index in the circle
*/
function _getMemberIndex(
address[] memory _members,
address _member
) internal pure returns (bool isMember, uint256 memberIndex) {
for (uint256 i = 0; i < _members.length; i++) {
if (_members[i] != _member) continue;
return (true, i);
}
}
}
3 changes: 2 additions & 1 deletion src/contracts/SavingCircles.sol
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,8 @@ contract SavingCircles is ISavingCircles, ReentrancyGuardUpgradeable, OwnableUpg
return _memberStates[_id][_member].lastDepositRound;
}

function memberIndex(uint256 _id, address _member) external view returns (uint256 index) {
/// @inheritdoc ISavingCircles
function memberIndex(uint256 _id, address _member) external view override returns (uint256 index) {
return _memberStates[_id][_member].memberIndex;
}

Expand Down
10 changes: 10 additions & 0 deletions src/interfaces/ISavingCircles.sol
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,16 @@ interface ISavingCircles {
*/
function isMemberWithdrawable(uint256 id, address member) external view returns (bool withdrawable);

/**
* @notice Get a member's zero-based payout round in a circle
* @dev Returns 0 for addresses that are not members, indistinguishable from the first member.
* Callers that need to tell them apart must also check membership.
* @param id The ID of the circle
* @param member The member to look up
* @return index The member's zero-based position in the payout order
*/
function memberIndex(uint256 id, address member) external view returns (uint256 index);

/**
* @notice Get the address of the withdrawable by
* @param id The ID of the circle
Expand Down
23 changes: 23 additions & 0 deletions test/unit/AutomaticSavingCircles.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,29 @@ contract AutomaticSavingCirclesUnit is SavingCirclesTestBase {
assertFalse(savingCircles.hasClaimed(baseCircleId, alice));
}

function test_BatchExecuteAutomatedClaims_ReportsNotWithdrawableForNonMember() external {
address outsider = makeAddr('outsider');
_depositRound(baseCircleId);
_enableAutomaticClaim(baseCircleId, outsider);

ISavingCircles.Circle memory circle = savingCircles.getCircle(baseCircleId);
vm.warp(circle.effectiveCircleStartTime + DEPOSIT_INTERVAL);

uint256[] memory circleIds = new uint256[](1);
address[] memory targetMembers = new address[](1);
circleIds[0] = baseCircleId;
targetMembers[0] = outsider;

vm.prank(automationExecutor);
vm.expectEmit(true, true, false, true, address(automaticSavingCircles));
emit IAutomaticSavingCircles.AutomatedClaimFailed(
baseCircleId, outsider, abi.encodeWithSelector(ISavingCircles.NotWithdrawable.selector)
);
automaticSavingCircles.batchExecuteAutomatedClaims(circleIds, targetMembers);

assertEq(token.balanceOf(outsider), 0);
}

function test_BatchExecuteAutomatedClaims_ReportsUnknownCircleBeforeDisabledClaim() external {
uint256 invalidCircleId = savingCircles.nextId();
uint256[] memory circleIds = new uint256[](1);
Expand Down
Loading