Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
de2cae6
add: improve gas + subgraph fields
sirpy Feb 5, 2026
235c3f8
feat: Implement bulk member addition for DirectPayments and UBI pools…
EmekaManuel Dec 9, 2025
5b08ad2
refactor: Add type assertions for contract instances and import hardh…
EmekaManuel Dec 9, 2025
b8bdbea
feat: Enhance member addition functionality in DirectPayments and UBI…
EmekaManuel Dec 10, 2025
65a4659
refactor: simplify bulk member addition per code review
EmekaManuel Dec 15, 2025
ebed27a
refactor: streamline member addition logic in DirectPayments and UBI …
EmekaManuel Dec 24, 2025
d40e2be
refactor: change addMember functions to public visibility in DirectPa…
EmekaManuel Jan 13, 2026
ad76940
refactor: simplify loop structure in addMembers functions across Dire…
EmekaManuel Jan 13, 2026
f617fb4
refactor: enhance test structure for DirectPayments by consolidating …
EmekaManuel Jan 13, 2026
4ec0eea
refactor: streamline member addition logic in DirectPayments and UBI …
EmekaManuel Jan 13, 2026
684b6b6
refactor: enhance member addition logic in UBIPool contract
EmekaManuel Jan 22, 2026
52d60b2
refactor: improve member addition error handling in UBIPool contract
EmekaManuel Feb 5, 2026
b000f57
fix: update mock function call in DirectPayments claim test for prope…
EmekaManuel Feb 5, 2026
c137ddd
chore(): eliminated duplicated code
EmekaManuel Feb 5, 2026
75f8d17
feat: fix feedback issues
EmekaManuel Feb 5, 2026
2075c0d
refactor: update member addition logic in DirectPaymentsFactory
EmekaManuel Feb 8, 2026
16a1fae
fix: allow uniquness validator to be 0
sirpy Feb 12, 2026
d2d0a41
fix: vercel
sirpy Feb 12, 2026
5e62c86
release: v1.4.0 bulk add members
sirpy Feb 12, 2026
6c99e06
add: verify dev contracts. upgrade hardhat verify
sirpy Mar 24, 2026
0062df2
release: 1.4.1
sirpy Mar 24, 2026
e84c550
fix: typechain
sirpy Mar 24, 2026
6419f0b
version bump
sirpy Mar 24, 2026
fe03edc
fix: remove unused
sirpy Apr 5, 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
2 changes: 1 addition & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@tanstack/react-query": "^5.66.0",
"@uniswap/router-sdk": "^1.9.3",
"@uniswap/sdk-core": "^5.3.1",
"@uniswap/smart-order-router": "^3.35.12",
"@uniswap/smart-order-router": "3.*",
"@uniswap/v3-sdk": "^3.13.1",
Comment on lines 36 to 38

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Relaxing the smart-order-router version to 3.* allows downgrades to older 3.x versions, which may miss APIs you rely on.

^3.35.12 guaranteed a minimum version with known APIs and fixes. 3.* permits older 3.x versions that may lack required functionality, particularly for consumers or installs without a reliable lockfile. If you want broader compatibility while preserving the tested floor, use a range like ">=3.35.12 <4" instead.

Suggested change
"@uniswap/sdk-core": "^5.3.1",
"@uniswap/smart-order-router": "^3.35.12",
"@uniswap/smart-order-router": "3.*",
"@uniswap/v3-sdk": "^3.13.1",
"@uniswap/sdk-core": "^5.3.1",
"@uniswap/smart-order-router": ">=3.35.12 <4",
"@uniswap/v3-sdk": "^3.13.1",

"@usedapp/core": "^1.2.10",
"@walletconnect/modal-react-native": "^1.0.0-rc.9",
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"address": "0x1E556Ca66AD4B3a9510a112dAb8Ef2529e28883E",
"address": "0x316eAeA9165FEA73F5B9fC65d7450b423c4213ae",
"abi": [
{
"inputs": [],
Expand Down Expand Up @@ -1336,4 +1336,4 @@
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"address": "0x1cCEE9f554d37ea4DB526108B57bDf00A2f5025f",
"address": "0x9A2012d5C21B46B27b6A9D98F112A8A27Bc2A853",
"abi": [
{
"inputs": [],
Expand Down Expand Up @@ -176,4 +176,4 @@
"storage": [],
"types": null
}
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,4 @@
"storage": [],
"types": null
}
}
}
9 changes: 3 additions & 6 deletions packages/contracts/deployments/development-celo/UBIPool.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"address": "0x2d176D1FB534d2380168f2B91610c313609ed521",
"address": "0xce543E1c4C1a70Fdf986c7a1A31478DC76b465E7",
"abi": [
{
"inputs": [],
Expand Down Expand Up @@ -1331,4 +1331,4 @@
}
}
}
}
}
21 changes: 14 additions & 7 deletions packages/contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import '@nomicfoundation/hardhat-toolbox';
import * as dotenv from 'dotenv';
import '@typechain/hardhat';
import 'hardhat-abi-exporter';
import 'hardhat-contract-sizer';
import '@openzeppelin/hardhat-upgrades';
import 'hardhat-deploy';
import 'hardhat-celo';
import '@nomicfoundation/hardhat-verify';
import { HardhatUserConfig } from 'hardhat/config';

dotenv.config();
Expand Down Expand Up @@ -112,13 +112,20 @@ const config: HardhatUserConfig = {
},
},
},
sourcify: {
enabled: true,
},
etherscan: {
apiKey: {
mainnet: process.env.ETHERSCAN_KEY || '',
celo: process.env.CELOSCAN_KEY || '',
alfajores: process.env.CELOSCAN_KEY || '',
},
apiKey: process.env.ETHERSCAN_KEY || '',
customChains: [
{
network: 'celo',
chainId: 42220,
urls: {
apiURL: 'https://api.etherscan.io/v2/api?chainid=42220',
browserURL: 'https://celoscan.io/',
},
},
{
network: 'alfajores',
chainId: 44787,
Expand Down
Loading
Loading