Skip to content

Commit

Permalink
add: finalize ubipool, add membership, add donate swap
Browse files Browse the repository at this point in the history
  • Loading branch information
sirpy committed Jul 4, 2024
1 parent 17baa74 commit 162b48d
Show file tree
Hide file tree
Showing 14 changed files with 384 additions and 173 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import "hardhat/console.sol";

contract DirectPaymentsFactory is AccessControlUpgradeable, UUPSUpgradeable {
error NOT_PROJECT_OWNER();
error NOT_POOL();

event PoolCreated(
address indexed pool,
Expand Down Expand Up @@ -45,6 +46,9 @@ contract DirectPaymentsFactory is AccessControlUpgradeable, UUPSUpgradeable {
address public feeRecipient;
uint32 public feeBps;

mapping(address => address[]) public memberPools;
address[] public pools;

modifier onlyProjectOwnerOrNon(string memory projectId) {
DirectPaymentsPool controlPool = projectIdToControlPool[keccak256(bytes(projectId))];
// console.log("result %s", controlPool.hasRole(controlPool.DEFAULT_ADMIN_ROLE(), msg.sender));
Expand All @@ -56,16 +60,21 @@ contract DirectPaymentsFactory is AccessControlUpgradeable, UUPSUpgradeable {
_;
}

modifier onlyProjectOwnerByPool(DirectPaymentsPool pool) {
string memory projectId = registry[address(pool)].projectId;
DirectPaymentsPool controlPool = projectIdToControlPool[keccak256(bytes(projectId))];
if (controlPool.hasRole(controlPool.DEFAULT_ADMIN_ROLE(), msg.sender) == false) {
modifier onlyPoolOwner(DirectPaymentsPool pool) {
if (pool.hasRole(pool.DEFAULT_ADMIN_ROLE(), msg.sender) == false) {
revert NOT_PROJECT_OWNER();
}

_;
}

modifier onlyPool() {
if (bytes(registry[msg.sender].projectId).length == 0) {
revert NOT_POOL();
}
_;
}

function _authorizeUpgrade(address _impl) internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}

function initialize(
Expand Down Expand Up @@ -138,12 +147,14 @@ contract DirectPaymentsFactory is AccessControlUpgradeable, UUPSUpgradeable {
registry[address(pool)].projectId = _projectId;

pool.renounceRole(DEFAULT_ADMIN_ROLE, address(this));
pools.push(address(pool));

emit PoolCreated(address(pool), _projectId, _ipfs, nextNftType, _settings, _limits);

nextNftType++;
}

function changePoolDetails(DirectPaymentsPool _pool, string memory _ipfs) external onlyProjectOwnerByPool(_pool) {
function changePoolDetails(DirectPaymentsPool _pool, string memory _ipfs) external onlyPoolOwner(_pool) {
registry[address(_pool)].ipfs = _ipfs;
emit PoolDetailsChanged(address(_pool), _ipfs);
}
Expand All @@ -162,4 +173,17 @@ contract DirectPaymentsFactory is AccessControlUpgradeable, UUPSUpgradeable {
feeBps = _feeBps;
feeRecipient = _feeRecipient;
}

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

function removeMember(address member) external onlyPool {
for (uint i = 0; i < memberPools[member].length; i++) {
if (memberPools[member][i] == msg.sender) {
memberPools[member][i] = memberPools[member][memberPools[member].length - 1];
memberPools[member].pop();
}
}
}
}
22 changes: 18 additions & 4 deletions packages/contracts/contracts/DirectPayments/DirectPaymentsPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface IMembersValidator {
}

interface IIdentityV2 {
function getWhitelistedRoot(address member) external returns (address);
function getWhitelistedRoot(address member) external view returns (address);
}

/**
Expand Down Expand Up @@ -120,8 +120,8 @@ contract DirectPaymentsPool is
*/
function _authorizeUpgrade(address impl) internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}

function getRegistry() public view override returns (DirectPaymentsFactory) {
return DirectPaymentsFactory(registry);
function getRegistry() public view override returns (IRegistry) {
return IRegistry(address(registry));
}

/**
Expand Down Expand Up @@ -341,10 +341,24 @@ contract DirectPaymentsPool is
}
}

_setupRole(MEMBER_ROLE, member);
_grantRole(MEMBER_ROLE, member);
return true;
}

function _grantRole(bytes32 role, address account) internal virtual override {
if (role == MEMBER_ROLE) {
registry.addMember(account);
}
super._grantRole(role, account);
}

function _revokeRole(bytes32 role, address account) internal virtual override {
if (role == MEMBER_ROLE) {
registry.removeMember(account);
}
super._revokeRole(role, account);
}

function mintNFT(address _to, ProvableNFT.NFTData memory _nftData, bool withClaim) external onlyRole(MINTER_ROLE) {
uint nftId = nft.mintPermissioned(_to, _nftData, true, "");
if (withClaim) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,6 @@ import "@uniswap/swap-router-contracts/contracts/interfaces/IV3SwapRouter.sol";
import "../DirectPayments/DirectPaymentsFactory.sol";
import "../utils/HelperLibrary.sol";

// import "hardhat/console.sol";

interface IRegistry {
function feeRecipient() external view returns (address);

function feeBps() external view returns (uint32);
}

abstract contract GoodCollectiveSuperApp is SuperAppBaseFlow {
int96 public constant MIN_FLOW_RATE = 386e9;

Expand Down Expand Up @@ -166,6 +158,28 @@ abstract contract GoodCollectiveSuperApp is SuperAppBaseFlow {
return _ctx;
}

/**
* @dev allow single contribution. user needs to approve tokens first. can be used in superfluid batch actions.
* @param _sender The address of the sender who is contributing tokens.
* @param _customData The SwapData struct containing information about the swap
* @param _ctx The context of the transaction for superfluid in case this was used in superfluid batch. otherwise can be empty.
* @return Returns the context of the transaction.
*/
function supportWithSwap(
address _sender,
HelperLibrary.SwapData memory _customData,
bytes memory _ctx
) external onlyHostOrSender(_sender) returns (bytes memory) {
uint256 balance = superToken.balanceOf(address(this));
HelperLibrary.handleSwap(swapRouter, _customData, address(superToken), _sender, address(this));
uint256 amountReceived = superToken.balanceOf(address(this)) - balance;
if (amountReceived == 0) revert ZERO_AMOUNT();

// Update the contribution amount for the sender in the supporters mapping
_updateSupporter(_sender, int256(amountReceived), 0, ""); //we pass empty ctx since this is not a flow but a single donation
return _ctx;
}

/**
* @dev Handles the swap of tokens using the SwapData struct
* @param _customData The SwapData struct containing information about the swap
Expand Down Expand Up @@ -253,7 +267,7 @@ abstract contract GoodCollectiveSuperApp is SuperAppBaseFlow {
) internal returns (bytes memory newCtx) {
newCtx = _ctx;
bool _isFlow = _ctx.length > 0;
_updateStats(_isFlow ? 0 : uint256(_previousFlowRateOrAmount));
HelperLibrary.updateStats(stats, superToken, getRegistry(), _isFlow ? 0 : uint256(_previousFlowRateOrAmount));
// Get the current flow rate for the supporter
int96 flowRate = superToken.getFlowRate(_supporter, address(this));
uint256 prevContribution = supporters[_supporter].contribution;
Expand All @@ -266,7 +280,14 @@ abstract contract GoodCollectiveSuperApp is SuperAppBaseFlow {
supporters[_supporter].contribution +=
uint96(int96(_previousFlowRateOrAmount)) *
(block.timestamp - _lastUpdated);
newCtx = _takeFeeFlow(flowRate - int96(_previousFlowRateOrAmount), _ctx);
newCtx = HelperLibrary.takeFeeFlow(
cfaV1,
stats,
superToken,
getRegistry(),
flowRate - int96(_previousFlowRateOrAmount),
_ctx
);
// we update the last rate after we do all changes to our own flows
stats.lastIncomeRate = superToken.getNetFlowRate(address(this));
} else {
Expand All @@ -284,51 +305,6 @@ abstract contract GoodCollectiveSuperApp is SuperAppBaseFlow {
);
}

// this should be called before any flow rate changes
function _updateStats(uint256 _amount) internal {
//use last rate before the current possible rate update
stats.netIncome += uint96(stats.lastIncomeRate) * (block.timestamp - stats.lastUpdate);
uint feeBps;
if (address(getRegistry()) != address(0)) {
feeBps = getRegistry().feeBps();
//fees sent to last recipient, the flowRate to recipient still wasnt updated.
stats.totalFees +=
uint96(superToken.getFlowRate(address(this), stats.lastFeeRecipient)) *
(block.timestamp - stats.lastUpdate);
}
if (_amount > 0) {
stats.netIncome += (_amount * (10000 - feeBps)) / 10000;
stats.totalFees += (_amount * feeBps) / 10000;
}
stats.lastUpdate = block.timestamp;
}

function _takeFeeFlow(int96 _diffRate, bytes memory _ctx) internal returns (bytes memory newCtx) {
newCtx = _ctx;
if (address(getRegistry()) == address(0)) return newCtx;
address recipient = getRegistry().feeRecipient();
int96 curFeeRate = superToken.getFlowRate(address(this), stats.lastFeeRecipient);
bool newRecipient;
if (recipient != stats.lastFeeRecipient) {
newRecipient = true;
if (stats.lastFeeRecipient != address(0)) {
//delete old recipient flow
if (curFeeRate > 0)
newCtx = cfaV1.deleteFlowWithCtx(newCtx, address(this), stats.lastFeeRecipient, superToken); //passing in the ctx which is sent to the callback here
}
stats.lastFeeRecipient = recipient;
}
if (recipient == address(0)) return newCtx;

int96 feeRateChange = (_diffRate * int32(getRegistry().feeBps())) / 10000;
int96 newFeeRate = curFeeRate + feeRateChange;
if (newFeeRate <= 0 && newRecipient == false) {
newCtx = cfaV1.deleteFlowWithCtx(newCtx, address(this), recipient, superToken); //passing in the ctx which is sent to the callback here
} else if (curFeeRate > 0 && newRecipient == false) {
newCtx = cfaV1.updateFlowWithCtx(newCtx, recipient, superToken, newFeeRate); //passing in the ctx which is sent to the callback here
} else if (newFeeRate > 0) newCtx = cfaV1.createFlowWithCtx(newCtx, recipient, superToken, newFeeRate); //passing in the ctx which is sent to the callback here
}

function _takeFeeSingle(uint256 _amount) internal {
if (address(getRegistry()) == address(0)) return;
address recipient = getRegistry().feeRecipient();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

interface IRegistry {
function feeRecipient() external view returns (address);

function feeBps() external view returns (uint32);
}

interface IGoodCollectiveSuperApp {
struct Stats {
uint256 netIncome; //without fees
Expand Down
54 changes: 25 additions & 29 deletions packages/contracts/contracts/UBI/UBIPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ contract UBIPool is AccessControlUpgradeable, GoodCollectiveSuperApp, UUPSUpgrad

error CLAIMFOR_DISABLED();
error NOT_MEMBER(address claimer);
error NOT_WHITELISTED(address claimer);
error ALREADY_CLAIMED(address claimer);
error NOT_WHITELISTED(address whitelistedRoot);
error ALREADY_CLAIMED(address whitelistedRoot);
error INVALID_0_VALUE();
error EMPTY_MANAGER();
error MAX_MEMBERS_REACHED();
error MAX_CLAIMERS_REACHED();

bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
bytes32 public constant MEMBER_ROLE = keccak256("MEMBER_ROLE");
Expand Down Expand Up @@ -70,7 +70,8 @@ contract UBIPool is AccessControlUpgradeable, GoodCollectiveSuperApp, UUPSUpgrad
// can you trigger claim for someone else
bool claimForEnabled;
uint maxClaimAmount;
uint32 maxMembers;
uint32 maxClaimers;
bool onlyMembers;
}

struct PoolStatus {
Expand All @@ -88,7 +89,7 @@ contract UBIPool is AccessControlUpgradeable, GoodCollectiveSuperApp, UUPSUpgrad
uint256 periodClaimers;
uint256 periodDistributed;
mapping(address => uint256) lastClaimed;
uint32 membersCount;
uint32 claimersCount;
}

PoolSettings public settings;
Expand Down Expand Up @@ -225,7 +226,12 @@ contract UBIPool is AccessControlUpgradeable, GoodCollectiveSuperApp, UUPSUpgrad
function _claim(address claimer, bool sendToWhitelistedRoot) internal {
address whitelistedRoot = IIdentityV2(settings.uniquenessValidator).getWhitelistedRoot(claimer);
if (whitelistedRoot == address(0)) revert NOT_WHITELISTED(claimer);
if (address(settings.membersValidator) != address(0) && hasRole(MEMBER_ROLE, claimer) == false)

// if open for anyone but has limits, we add the first claimers as members to handle the max claimers
if ((ubiSettings.maxClaimers > 0 && ubiSettings.onlyMembers == false)) _grantRole(MEMBER_ROLE, claimer);

// check membership if has claimers limits or limited to members only
if ((ubiSettings.maxClaimers > 0 || ubiSettings.onlyMembers) && hasRole(MEMBER_ROLE, claimer) == false)
revert NOT_MEMBER(claimer);

// calculats the formula up today ie on day 0 there are no active users, on day 1 any user
Expand All @@ -246,56 +252,46 @@ contract UBIPool is AccessControlUpgradeable, GoodCollectiveSuperApp, UUPSUpgrad
emit UBIClaimed(whitelistedRoot, claimer, dailyUbi);
}

function addMemberByManager(address member) external onlyRole(MANAGER_ROLE) returns (bool) {
if (hasRole(MEMBER_ROLE, member)) return true;

if (address(settings.uniquenessValidator) != address(0)) {
address rootAddress = settings.uniquenessValidator.getWhitelistedRoot(member);
if (rootAddress == address(0)) revert NOT_WHITELISTED(member);
}

_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) external returns (bool isMember) {
if (hasRole(MEMBER_ROLE, member)) return true;

if (address(settings.uniquenessValidator) != address(0)) {
address rootAddress = settings.uniquenessValidator.getWhitelistedRoot(member);
if (rootAddress == address(0)) revert NOT_WHITELISTED(member);
}

// if no members validator then anyone can join the pool
if (address(settings.membersValidator) != address(0)) {
if (address(settings.membersValidator) != address(0) && hasRole(MANAGER_ROLE, msg.sender) == false) {
if (settings.membersValidator.isMemberValid(address(this), msg.sender, member, extraData) == false) {
return false;
revert NOT_MEMBER(member);
}
}
// 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_MEMBER(member);
}

_grantRole(MEMBER_ROLE, member);
return true;
}

function _grantRole(bytes32 role, address account) internal virtual override {
if (role == MEMBER_ROLE) {
if (role == MEMBER_ROLE && hasRole(MEMBER_ROLE, account) == false) {
if (ubiSettings.maxClaimers > 0 && status.claimersCount > ubiSettings.maxClaimers)
revert MAX_CLAIMERS_REACHED();
registry.addMember(account);
if (ubiSettings.maxMembers > 0 && status.membersCount > ubiSettings.maxMembers)
revert MAX_MEMBERS_REACHED();
status.membersCount += 1;
status.claimersCount += 1;
}
super._grantRole(role, account);
}

function _revokeRole(bytes32 role, address account) internal virtual override {
if (role == MEMBER_ROLE) {
status.membersCount -= 1;
if (role == MEMBER_ROLE && hasRole(MEMBER_ROLE, account)) {
status.claimersCount -= 1;
registry.removeMember(account);
}
super._revokeRole(role, account);
}
Expand Down
9 changes: 9 additions & 0 deletions packages/contracts/contracts/UBI/UBIPoolFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@ contract UBIPoolFactory is AccessControlUpgradeable, UUPSUpgradeable {
memberPools[account].push(msg.sender);
}

function removeMember(address member) external onlyPool {
for (uint i = 0; i < memberPools[member].length; i++) {
if (memberPools[member][i] == msg.sender) {
memberPools[member][i] = memberPools[member][memberPools[member].length - 1];
memberPools[member].pop();
}
}
}

function getMemberPools(address member) external view returns (address[] memory) {
return memberPools[member];
}
Expand Down
Loading

0 comments on commit 162b48d

Please sign in to comment.