Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
cacf984
feat: Implement bulk member addition for DirectPayments and UBI pools…
EmekaManuel Dec 9, 2025
5a0defe
refactor: Add type assertions for contract instances and import hardh…
EmekaManuel Dec 9, 2025
15c42c0
feat: Enhance member addition functionality in DirectPayments and UBI…
EmekaManuel Dec 10, 2025
e4add4e
refactor: simplify bulk member addition per code review
EmekaManuel Dec 15, 2025
83e9e12
refactor: streamline member addition logic in DirectPayments and UBI …
EmekaManuel Dec 24, 2025
1f64306
refactor: change addMember functions to public visibility in DirectPa…
EmekaManuel Jan 13, 2026
9823949
refactor: simplify loop structure in addMembers functions across Dire…
EmekaManuel Jan 13, 2026
731c61d
refactor: enhance test structure for DirectPayments by consolidating …
EmekaManuel Jan 13, 2026
f6103f2
refactor: streamline member addition logic in DirectPayments and UBI …
EmekaManuel Jan 13, 2026
c8c9b7d
refactor: enhance member addition logic in UBIPool contract
EmekaManuel Jan 22, 2026
c6cb27f
refactor: improve member addition error handling in UBIPool contract
EmekaManuel Feb 5, 2026
e370354
fix: update mock function call in DirectPayments claim test for prope…
EmekaManuel Feb 5, 2026
c0deb2c
chore(): eliminated duplicated code
EmekaManuel Feb 5, 2026
f571d28
feat: fix feedback issues
EmekaManuel Feb 5, 2026
07e2ae1
refactor: update member addition logic in DirectPaymentsFactory
EmekaManuel Feb 8, 2026
ac2afe2
fix: allow uniquness validator to be 0
sirpy Feb 12, 2026
97ec887
fix: vercel
sirpy Feb 12, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ contract DirectPaymentsFactory is AccessControlUpgradeable, UUPSUpgradeable {
event PoolDetailsChanged(address indexed pool, string ipfs);
event PoolVerifiedChanged(address indexed pool, bool isVerified);
event UpdatedImpl(address indexed impl);
event MemberAdded(address indexed member, address indexed pool);

struct PoolRegistry {
string ipfs;
Expand Down Expand Up @@ -189,6 +190,15 @@ contract DirectPaymentsFactory is AccessControlUpgradeable, UUPSUpgradeable {

function addMember(address member) external onlyPool {
memberPools[member].push(msg.sender);
emit MemberAdded(member, msg.sender);
}

function addMembers(address[] calldata members) external onlyPool {
for (uint i = 0; i < members.length; ) {
memberPools[members[i]].push(msg.sender);
emit MemberAdded(members[i], msg.sender);
unchecked { ++i; }
}
}

function removeMember(address member) external onlyPool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils
import { IERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol";
import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import { IERC721ReceiverUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol";
import {
IERC721ReceiverUpgradeable
} from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol";

import { ProvableNFT } from "./ProvableNFT.sol";
import { DirectPaymentsFactory } from "./DirectPaymentsFactory.sol";
Expand Down Expand Up @@ -48,6 +50,7 @@ contract DirectPaymentsPool is
error NO_BALANCE();
error NFTTYPE_CHANGED();
error EMPTY_MANAGER();
error LENGTH_MISMATCH();

bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
bytes32 public constant MEMBER_ROLE = keccak256("MEMBER_ROLE");
Expand Down Expand Up @@ -75,6 +78,7 @@ contract DirectPaymentsPool is
);
event NFTClaimed(uint256 indexed tokenId, uint256 totalRewards);
event NOT_MEMBER_OR_WHITELISTED_OR_LIMITS(address contributer);
event MemberAdded(address indexed member);

// Define functions
struct PoolSettings {
Expand Down Expand Up @@ -214,6 +218,27 @@ contract DirectPaymentsPool is
return true;
}

/**
* @dev Adds multiple members to the pool in a single transaction.
* @param members Array of member addresses to add.
* @param extraData Array of additional validation data for each member.
*/
function addMembers(address[] calldata members, bytes[] calldata extraData) external onlyRole(MANAGER_ROLE) {
if (members.length != extraData.length) revert LENGTH_MISMATCH();

for (uint i = 0; i < members.length; ) {
bool added = _addMember(members[i], extraData[i]);
if (added) {
emit MemberAdded(members[i]);
}
unchecked {
++i;
}
}
}



function _grantRole(bytes32 role, address account) internal virtual override {
if (role == MEMBER_ROLE) {
registry.addMember(account);
Expand Down
46 changes: 41 additions & 5 deletions packages/contracts/contracts/UBI/UBIPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils
import { IERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol";
import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import { IERC721ReceiverUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol";
import {
IERC721ReceiverUpgradeable
} from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol";

import "../GoodCollective/GoodCollectiveSuperApp.sol";
import "./UBIPoolFactory.sol";
Expand All @@ -23,6 +25,7 @@ contract UBIPool is AccessControlUpgradeable, GoodCollectiveSuperApp, UUPSUpgrad
error EMPTY_MANAGER();
error MAX_MEMBERS_REACHED();
error MAX_PERIOD_CLAIMERS_REACHED(uint256 claimers);
error LENGTH_MISMATCH();

bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
bytes32 public constant MEMBER_ROLE = keccak256("MEMBER_ROLE");
Expand All @@ -42,6 +45,7 @@ contract UBIPool is AccessControlUpgradeable, GoodCollectiveSuperApp, UUPSUpgrad
event UBICycleCalculated(uint256 day, uint256 pool, uint256 cycleLength, uint256 dailyUBIPool);

event UBIClaimed(address indexed whitelistedRoot, address indexed claimer, uint256 amount);
event MemberAdded(address indexed member);

struct UBISettings {
//number of days of each UBI pool cycle
Expand Down Expand Up @@ -189,10 +193,8 @@ contract UBIPool is AccessControlUpgradeable, GoodCollectiveSuperApp, UUPSUpgrad

nextPeriodPool = status.dailyCyclePool;
nextDailyUbi;
if (
(currentDayInCycle() + 1) >= status.currentCycleLength || shouldStartEarlyCycle
) //start of cycle or first time
{
if ((currentDayInCycle() + 1) >= status.currentCycleLength || shouldStartEarlyCycle) {
//start of cycle or first time
nextPeriodPool = currentBalance / ubiSettings.cycleLengthDays;
newCycle = true;
}
Expand Down Expand Up @@ -296,10 +298,44 @@ contract UBIPool is AccessControlUpgradeable, GoodCollectiveSuperApp, UUPSUpgrad
return true;
}

/**
* @dev Adds multiple members to the pool in a single transaction.
* @param members Array of member addresses to add.
* @param extraData Array of additional validation data for each member.
*/
function addMembers(address[] calldata members, bytes[] calldata extraData) external onlyRole(MANAGER_ROLE) {
if (members.length != extraData.length) revert LENGTH_MISMATCH();

for (uint i = 0; i < members.length; ) {
address member = members[i];

// Validate uniqueness if validator is set
if (address(settings.uniquenessValidator) != address(0)) {
address rootAddress = settings.uniquenessValidator.getWhitelistedRoot(member);
if (rootAddress == address(0)) revert NOT_WHITELISTED(member);
}

// Validate with members validator if set (manager can bypass)
if (address(settings.membersValidator) != address(0)) {
if (!settings.membersValidator.isMemberValid(address(this), msg.sender, member, extraData[i])) {
revert NOT_MEMBER(member);
}
}

_grantRole(MEMBER_ROLE, member);

unchecked {
++i;
}
}
}

function removeMember(address member) external onlyRole(MANAGER_ROLE) {
_revokeRole(MEMBER_ROLE, member);
}



function _grantRole(bytes32 role, address account) internal virtual override {
if (role == MEMBER_ROLE && hasRole(MEMBER_ROLE, account) == false) {
if (ubiSettings.maxMembers > 0 && status.membersCount > ubiSettings.maxMembers)
Expand Down
10 changes: 10 additions & 0 deletions packages/contracts/contracts/UBI/UBIPoolFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ contract UBIPoolFactory is AccessControlUpgradeable, UUPSUpgradeable {
event PoolDetailsChanged(address indexed pool, string ipfs);
event PoolVerifiedChanged(address indexed pool, bool isVerified);
event UpdatedImpl(address indexed impl);
event MemberAdded(address indexed member, address indexed pool);

struct PoolRegistry {
string ipfs;
Expand Down Expand Up @@ -172,6 +173,15 @@ contract UBIPoolFactory is AccessControlUpgradeable, UUPSUpgradeable {

function addMember(address account) external onlyPool {
memberPools[account].push(msg.sender);
emit MemberAdded(account, msg.sender);
}

function addMembers(address[] calldata members) external onlyPool {
for (uint i = 0; i < members.length; ) {
memberPools[members[i]].push(msg.sender);
emit MemberAdded(members[i], msg.sender);
unchecked { ++i; }
}
}

function removeMember(address member) external onlyPool {
Expand Down
Loading
Loading