Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,6 @@ yarn-error.log*
**/types
packages/sdk-js/.yarn/install-state.gz
.vercel
!.vercel/project.json
.vscode/settings.json
packages/contracts/deployments/*/solcInputs
16 changes: 16 additions & 0 deletions .vercel/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"projectId": "prj_WbZ26ibOrzAavodIzIRbg30YCzF0",
"orgId": "team_0fLkvhhsRuNmvdbakI3Z7r9e",
"projectName": "good-collective",
"settings": {
"createdAt": 1691023624297,
"framework": null,
"devCommand": null,
"installCommand": "yarn",
"buildCommand": "yarn build:web",
"outputDirectory": null,
"rootDirectory": "packages/app",
"directoryListing": false,
"nodeVersion": "20.x"
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"shx": "^0.3.4",
"syncpack": "^8.2.4",
"ts-node": "^10.9.1",
"vercel": "^37.1.1"
"vercel": "^50.15.1"
},
"lint-staged": {
"packages/app/**/*.{ts,tsx}": [
Expand Down
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 @@ -187,8 +188,19 @@ contract DirectPaymentsFactory is AccessControlUpgradeable, UUPSUpgradeable {
feeRecipient = _feeRecipient;
}

function addMember(address member) external onlyPool {
function addMember(address member) public onlyPool {
_addMemberToRegistry(member);
}

function _addMemberToRegistry(address member) internal {
memberPools[member].push(msg.sender);
emit MemberAdded(member, msg.sender);
}

function addMembers(address[] calldata members) external onlyPool {
for (uint i = 0; i < members.length; i++) {
_addMemberToRegistry(members[i]);
}
}

function removeMember(address member) external onlyPool {
Expand Down
22 changes: 22 additions & 0 deletions packages/contracts/contracts/DirectPayments/DirectPaymentsPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,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 All @@ -74,6 +75,14 @@ contract DirectPaymentsPool is
uint256 rewardPerContributer
);
event NFTClaimed(uint256 indexed tokenId, uint256 totalRewards);
/**
* @dev Emitted when a contributor is skipped during reward distribution.
* This occurs when a contributor is either:
* - Not a member of the pool (does not have MEMBER_ROLE)
* - Not whitelisted (uniquenessValidator returns address(0))
* - Exceeds member limits (daily or monthly limits exceeded)
* @param contributer The address of the contributor that was skipped
*/
event NOT_MEMBER_OR_WHITELISTED_OR_LIMITS(address contributer);

// Define functions
Expand Down Expand Up @@ -214,6 +223,19 @@ 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; i++) {
_addMember(members[i], extraData[i]);
}
}

function _grantRole(bytes32 role, address account) internal virtual override {
if (role == MEMBER_ROLE) {
registry.addMember(account);
Expand Down
80 changes: 62 additions & 18 deletions packages/contracts/contracts/UBI/UBIPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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 "../GoodCollective/GoodCollectiveSuperApp.sol";
import "./UBIPoolFactory.sol";
Expand All @@ -23,6 +22,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 Down Expand Up @@ -189,10 +189,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 @@ -271,38 +269,83 @@ contract UBIPool is AccessControlUpgradeable, GoodCollectiveSuperApp, UUPSUpgrad
}

/**
* @dev Adds a member to the contract.
* @dev Internal function to add a member with validation.
* Always validates members, even when called by manager.
* @param member The address of the member to add.
* @param extraData Additional data to validate the member.
* @return isMember True if member was added, false if validation failed.
*/
function _addMember(address member, bytes memory extraData) internal returns (bool isMember) {
if (hasRole(MEMBER_ROLE, member)) return true;

function addMember(address member, bytes memory extraData) external returns (bool isMember) {
if (address(settings.uniquenessValidator) != address(0)) {
address rootAddress = settings.uniquenessValidator.getWhitelistedRoot(member);
if (rootAddress == address(0)) revert NOT_WHITELISTED(member);
if (rootAddress == address(0)) return false;
}

if (address(settings.membersValidator) != address(0) && hasRole(MANAGER_ROLE, msg.sender) == false) {
// Always check membersValidator if it exists, regardless of caller role
if (address(settings.membersValidator) != address(0)) {
if (settings.membersValidator.isMemberValid(address(this), msg.sender, member, extraData) == false) {
revert NOT_MEMBER(member);
return false;
}
}
// if no members validator then if members only only manager can add members
else if (ubiSettings.onlyMembers && hasRole(MANAGER_ROLE, msg.sender) == false) {
revert NOT_MANAGER(member);
return false;
}

_grantRole(MEMBER_ROLE, member);
return true;
}

/**
* @dev Adds a member to the contract.
* @param member The address of the member to add.
* @param extraData Additional data to validate the member.
*/
function addMember(address member, bytes memory extraData) public returns (bool isMember) {
bool success = _addMember(member, extraData);

if (!success) {
// Determine the specific error to revert with
if (address(settings.uniquenessValidator) != address(0)) {
address rootAddress = settings.uniquenessValidator.getWhitelistedRoot(member);
if (rootAddress == address(0)) revert NOT_WHITELISTED(member);
}

if (address(settings.membersValidator) != address(0) && hasRole(MANAGER_ROLE, msg.sender) == false) {
revert NOT_MEMBER(member);
}

if (ubiSettings.onlyMembers && hasRole(MANAGER_ROLE, msg.sender) == false) {
revert NOT_MANAGER(member);
}
}

return success;
}

/**
* @dev Adds multiple members to the pool in a single transaction.
* Invalid members are skipped instead of causing the transaction to revert.
* @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; i++) {
_addMember(members[i], extraData[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)
if (ubiSettings.maxMembers > 0 && status.membersCount >= ubiSettings.maxMembers)
revert MAX_MEMBERS_REACHED();
registry.addMember(account);
status.membersCount += 1;
Expand Down Expand Up @@ -357,11 +400,8 @@ contract UBIPool is AccessControlUpgradeable, GoodCollectiveSuperApp, UUPSUpgrad
}

function _verifyPoolSettings(PoolSettings memory _poolSettings) internal pure {
if (
_poolSettings.manager == address(0) ||
address(_poolSettings.uniquenessValidator) == address(0) ||
address(_poolSettings.rewardToken) == address(0)
) revert INVALID_0_VALUE();
if (_poolSettings.manager == address(0) || address(_poolSettings.rewardToken) == address(0))
revert INVALID_0_VALUE();
}

function estimateNextDailyUBI() public view returns (uint256 nextDailyUbi) {
Expand Down Expand Up @@ -391,7 +431,11 @@ contract UBIPool is AccessControlUpgradeable, GoodCollectiveSuperApp, UUPSUpgrad
}

function hasClaimed(address _member) public view returns (bool) {
address whitelistedRoot = IIdentityV2(settings.uniquenessValidator).getWhitelistedRoot(_member);
address whitelistedRoot = _member;
if (address(settings.uniquenessValidator) != address(0)) {
whitelistedRoot = IIdentityV2(settings.uniquenessValidator).getWhitelistedRoot(_member);
}
whitelistedRoot = IIdentityV2(settings.uniquenessValidator).getWhitelistedRoot(_member);
return status.lastClaimed[whitelistedRoot] == getCurrentDay();
}

Expand Down
14 changes: 13 additions & 1 deletion 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 @@ -170,8 +171,19 @@ contract UBIPoolFactory is AccessControlUpgradeable, UUPSUpgradeable {
feeRecipient = _feeRecipient;
}

function addMember(address account) external onlyPool {
function addMember(address account) public onlyPool {
_addMemberToRegistry(account);
}

function _addMemberToRegistry(address account) internal {
memberPools[account].push(msg.sender);
emit MemberAdded(account, msg.sender);
}

function addMembers(address[] calldata members) external onlyPool {
for (uint i = 0; i < members.length; i++) {
addMember(members[i]);
}
}

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