From 162b48dd03a1e5ef7fc81cad7dbc647f0508b723 Mon Sep 17 00:00:00 2001 From: sirpy Date: Thu, 4 Jul 2024 12:22:29 +0300 Subject: [PATCH] add: finalize ubipool, add membership, add donate swap --- .../DirectPayments/DirectPaymentsFactory.sol | 34 ++++++-- .../DirectPayments/DirectPaymentsPool.sol | 22 ++++- .../GoodCollective/GoodCollectiveSuperApp.sol | 86 +++++++------------ .../IGoodCollectiveSuperApp.sol | 6 ++ packages/contracts/contracts/UBI/UBIPool.sol | 54 ++++++------ .../contracts/UBI/UBIPoolFactory.sol | 9 ++ .../contracts/utils/HelperLibrary.sol | 78 +++++++++++++++-- packages/contracts/deploy/00.Mocks.deploy.ts | 53 ++++++++++++ ....deploy.ts => 01.DirectPayments.deploy.ts} | 30 ++----- ...UBIPool.deploy.ts => 02.UBIPool.deploy.ts} | 28 ++---- packages/contracts/package.json | 4 +- packages/contracts/scripts/verify.ts | 82 +++++++++++++++--- .../DirectPayments.claim.test.ts | 38 ++++---- .../DirectPayments.superapp.test.ts | 33 +++++++ 14 files changed, 384 insertions(+), 173 deletions(-) create mode 100644 packages/contracts/deploy/00.Mocks.deploy.ts rename packages/contracts/deploy/{00.DirectPayments.deploy.ts => 01.DirectPayments.deploy.ts} (71%) rename packages/contracts/deploy/{01.UBIPool.deploy.ts => 02.UBIPool.deploy.ts} (69%) diff --git a/packages/contracts/contracts/DirectPayments/DirectPaymentsFactory.sol b/packages/contracts/contracts/DirectPayments/DirectPaymentsFactory.sol index ee387399..83309b60 100644 --- a/packages/contracts/contracts/DirectPayments/DirectPaymentsFactory.sol +++ b/packages/contracts/contracts/DirectPayments/DirectPaymentsFactory.sol @@ -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, @@ -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)); @@ -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( @@ -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); } @@ -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(); + } + } + } } diff --git a/packages/contracts/contracts/DirectPayments/DirectPaymentsPool.sol b/packages/contracts/contracts/DirectPayments/DirectPaymentsPool.sol index 11546697..37f8e48f 100644 --- a/packages/contracts/contracts/DirectPayments/DirectPaymentsPool.sol +++ b/packages/contracts/contracts/DirectPayments/DirectPaymentsPool.sol @@ -21,7 +21,7 @@ interface IMembersValidator { } interface IIdentityV2 { - function getWhitelistedRoot(address member) external returns (address); + function getWhitelistedRoot(address member) external view returns (address); } /** @@ -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)); } /** @@ -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) { diff --git a/packages/contracts/contracts/GoodCollective/GoodCollectiveSuperApp.sol b/packages/contracts/contracts/GoodCollective/GoodCollectiveSuperApp.sol index 904db204..b4e4c2d4 100644 --- a/packages/contracts/contracts/GoodCollective/GoodCollectiveSuperApp.sol +++ b/packages/contracts/contracts/GoodCollective/GoodCollectiveSuperApp.sol @@ -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; @@ -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 @@ -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; @@ -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 { @@ -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(); diff --git a/packages/contracts/contracts/GoodCollective/IGoodCollectiveSuperApp.sol b/packages/contracts/contracts/GoodCollective/IGoodCollectiveSuperApp.sol index ac0997da..146e5096 100644 --- a/packages/contracts/contracts/GoodCollective/IGoodCollectiveSuperApp.sol +++ b/packages/contracts/contracts/GoodCollective/IGoodCollectiveSuperApp.sol @@ -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 diff --git a/packages/contracts/contracts/UBI/UBIPool.sol b/packages/contracts/contracts/UBI/UBIPool.sol index b78f61a7..b6b76b91 100644 --- a/packages/contracts/contracts/UBI/UBIPool.sol +++ b/packages/contracts/contracts/UBI/UBIPool.sol @@ -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"); @@ -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 { @@ -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; @@ -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 @@ -246,18 +252,6 @@ 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. @@ -265,37 +259,39 @@ contract UBIPool is AccessControlUpgradeable, GoodCollectiveSuperApp, UUPSUpgrad */ 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); } diff --git a/packages/contracts/contracts/UBI/UBIPoolFactory.sol b/packages/contracts/contracts/UBI/UBIPoolFactory.sol index 2b89b72a..ba7e9db9 100644 --- a/packages/contracts/contracts/UBI/UBIPoolFactory.sol +++ b/packages/contracts/contracts/UBI/UBIPoolFactory.sol @@ -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]; } diff --git a/packages/contracts/contracts/utils/HelperLibrary.sol b/packages/contracts/contracts/utils/HelperLibrary.sol index 496c2083..84053ba2 100644 --- a/packages/contracts/contracts/utils/HelperLibrary.sol +++ b/packages/contracts/contracts/utils/HelperLibrary.sol @@ -6,11 +6,13 @@ import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; import "@uniswap/swap-router-contracts/contracts/interfaces/IV3SwapRouter.sol"; import { ISuperToken } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; import { SuperTokenV1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; +import { CFAv1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/CFAv1Library.sol"; import "../GoodCollective/IGoodCollectiveSuperApp.sol"; library HelperLibrary { using SuperTokenV1Library for ISuperToken; + using CFAv1Library for CFAv1Library.InitData; /** * @dev A struct containing information about a token swap @@ -33,7 +35,17 @@ library HelperLibrary { SwapData memory _customData, address outTokenIfNoPath, address _sender - ) external { + ) external returns (uint256 amountOut) { + return handleSwap(swapRouter, _customData, outTokenIfNoPath, _sender, _sender); + } + + function handleSwap( + IV3SwapRouter swapRouter, + SwapData memory _customData, + address outTokenIfNoPath, + address _sender, + address _recipient + ) public returns (uint256 amountOut) { // Transfer the tokens from the sender to this contract TransferHelper.safeTransferFrom(_customData.swapFrom, _sender, address(this), _customData.amount); @@ -44,25 +56,25 @@ library HelperLibrary { // If a path is provided, execute a multi-hop swap IV3SwapRouter.ExactInputParams memory params = IV3SwapRouter.ExactInputParams({ path: _customData.path, - recipient: _sender, + recipient: _recipient, amountIn: _customData.amount, amountOutMinimum: _customData.minReturn }); - swapRouter.exactInput(params); + return swapRouter.exactInput(params); } else { // If no path is provided, execute a single-hop swap IV3SwapRouter.ExactInputSingleParams memory params = IV3SwapRouter.ExactInputSingleParams({ tokenIn: _customData.swapFrom, tokenOut: outTokenIfNoPath, fee: 10000, - recipient: _sender, + recipient: _recipient, amountIn: _customData.amount, amountOutMinimum: _customData.minReturn, sqrtPriceLimitX96: 0 }); // Execute the swap using `exactInputSingle` - swapRouter.exactInputSingle(params); + return swapRouter.exactInputSingle(params); } } @@ -78,4 +90,60 @@ library HelperLibrary { uint96(superToken.getFlowRate(address(this), stats.lastFeeRecipient)) * (block.timestamp - stats.lastUpdate); } + + // this should be called before any flow rate changes + function updateStats( + IGoodCollectiveSuperApp.Stats storage stats, + ISuperToken superToken, + IRegistry registry, + uint256 _amount + ) external { + //use last rate before the current possible rate update + stats.netIncome += uint96(stats.lastIncomeRate) * (block.timestamp - stats.lastUpdate); + uint feeBps; + if (address(registry) != address(0)) { + feeBps = registry.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( + CFAv1Library.InitData storage cfaV1, + IGoodCollectiveSuperApp.Stats storage stats, + ISuperToken superToken, + IRegistry registry, + int96 _diffRate, + bytes memory _ctx + ) public returns (bytes memory newCtx) { + newCtx = _ctx; + if (address(registry) == address(0)) return newCtx; + address recipient = registry.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 newFeeRate = curFeeRate + (_diffRate * int32(registry.feeBps())) / 10000; + 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 + } } diff --git a/packages/contracts/deploy/00.Mocks.deploy.ts b/packages/contracts/deploy/00.Mocks.deploy.ts new file mode 100644 index 00000000..7dcd9f4d --- /dev/null +++ b/packages/contracts/deploy/00.Mocks.deploy.ts @@ -0,0 +1,53 @@ +import { ethers, network } from 'hardhat'; +import { DeployFunction } from 'hardhat-deploy/types'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { deployTestFramework } from '@superfluid-finance/ethereum-contracts/dev-scripts/deploy-test-framework'; +import { deploySuperGoodDollar } from '@gooddollar/goodprotocol'; +import { FormatTypes } from 'ethers/lib/utils'; + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre; + const { deploy, execute } = deployments; + const { deployer } = await getNamedAccounts(); + + let sfHost; + let swapMock; + if (hre.network.live === false) { + const { frameworkDeployer } = await deployTestFramework(); + const sfFramework = await frameworkDeployer.getFramework(); + console.log("host", sfFramework.host) + const signers = await ethers.getSigners(); + const gdframework = await deploySuperGoodDollar(signers[0], sfFramework, [ + ethers.constants.AddressZero, + ethers.constants.AddressZero, + ]); + + console.log("deployed gd host:", await gdframework.GoodDollar.getHost()) + swapMock = await deploy('SwapRouterMock', { + from: deployer, + log: true, + args: [gdframework.GoodDollar.address], + }); + await deployments.save('GoodDollar', { + abi: (gdframework.GoodDollar.interface as any).format(FormatTypes.full), + address: gdframework.GoodDollar.address, + }); + await deployments.save('SuperFluidResolver', { abi: [], address: sfFramework.resolver }); + await gdframework.GoodDollar.mint(swapMock.address, ethers.constants.WeiPerEther.mul(100000)); + await gdframework.GoodDollar.mint(deployer, ethers.constants.WeiPerEther.mul(100000)); + sfHost = sfFramework.host; + console.log("deployed test gd and sf host", gdframework.GoodDollar.address, sfHost) + } +}; + +export default func; +func.tags = ['Test']; + +/* +Tenderly verification +let verification = await tenderly.verify({ + name: contractName, + address: contractAddress, + network: targetNetwork, +}); +*/ diff --git a/packages/contracts/deploy/00.DirectPayments.deploy.ts b/packages/contracts/deploy/01.DirectPayments.deploy.ts similarity index 71% rename from packages/contracts/deploy/00.DirectPayments.deploy.ts rename to packages/contracts/deploy/01.DirectPayments.deploy.ts index 4e771ccb..9466e512 100644 --- a/packages/contracts/deploy/00.DirectPayments.deploy.ts +++ b/packages/contracts/deploy/01.DirectPayments.deploy.ts @@ -18,28 +18,10 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { let feeRecipient = GDContracts[hre.network.name]?.UBIScheme || deployer; let feeBps = 1000; if (hre.network.live === false) { - const { frameworkDeployer } = await deployTestFramework(); - const sfFramework = await frameworkDeployer.getFramework(); - - const signers = await ethers.getSigners(); - const gdframework = await deploySuperGoodDollar(signers[0], sfFramework, [ - ethers.constants.AddressZero, - ethers.constants.AddressZero, - ]); - - swapMock = await deploy('SwapRouterMock', { - from: deployer, - log: true, - args: [gdframework.GoodDollar.address], - }); - await deployments.save('GoodDollar', { - abi: (gdframework.GoodDollar.interface as any).format(FormatTypes.full), - address: gdframework.GoodDollar.address, - }); - await deployments.save('SuperFluidResolver', { abi: [], address: sfFramework.resolver }); - await gdframework.GoodDollar.mint(swapMock.address, ethers.constants.WeiPerEther.mul(100000)); - await gdframework.GoodDollar.mint(deployer, ethers.constants.WeiPerEther.mul(100000)); - sfHost = sfFramework.host; + swapMock = (await deployments.get("SwapRouterMock")).address + const gd = await ethers.getContractAt("ISuperGoodDollar", (await deployments.get("GoodDollar")).address) + sfHost = await gd.getHost() + console.log("deployed test gd and sf host", gd.address, sfHost, swapMock) } else { const sfFramework = await Framework.create({ chainId: network.config.chainId || 0, @@ -54,11 +36,11 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { log: true, }); - console.log('deploying pool impl', [sfHost, swapMock?.address || '0x5615CDAb10dc425a742d643d949a7F474C01abc4']); + console.log('deploying pool impl', [sfHost, swapMock || '0x5615CDAb10dc425a742d643d949a7F474C01abc4']); const pool = await deploy('DirectPaymentsPool', { // Learn more about args here: https://www.npmjs.com/package/hardhat-deploy#deploymentsdeploy from: deployer, - args: [sfHost, swapMock?.address || '0x5615CDAb10dc425a742d643d949a7F474C01abc4'], //uniswap on celo + args: [sfHost, swapMock || '0x5615CDAb10dc425a742d643d949a7F474C01abc4'], //uniswap on celo log: true, libraries: { HelperLibrary: helplib.address, diff --git a/packages/contracts/deploy/01.UBIPool.deploy.ts b/packages/contracts/deploy/02.UBIPool.deploy.ts similarity index 69% rename from packages/contracts/deploy/01.UBIPool.deploy.ts rename to packages/contracts/deploy/02.UBIPool.deploy.ts index 90669ffd..b362bfdb 100644 --- a/packages/contracts/deploy/01.UBIPool.deploy.ts +++ b/packages/contracts/deploy/02.UBIPool.deploy.ts @@ -18,28 +18,10 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { let feeRecipient = GDContracts[hre.network.name]?.UBIScheme || deployer; let feeBps = 1000; if (hre.network.live === false) { - const { frameworkDeployer } = await deployTestFramework(); - const sfFramework = await frameworkDeployer.getFramework(); - - const signers = await ethers.getSigners(); - const gdframework = await deploySuperGoodDollar(signers[0], sfFramework, [ - ethers.constants.AddressZero, - ethers.constants.AddressZero, - ]); - - swapMock = await deploy('SwapRouterMock', { - from: deployer, - log: true, - args: [gdframework.GoodDollar.address], - }); - await deployments.save('GoodDollar', { - abi: (gdframework.GoodDollar.interface as any).format(FormatTypes.full), - address: gdframework.GoodDollar.address, - }); - await deployments.save('SuperFluidResolver', { abi: [], address: sfFramework.resolver }); - await gdframework.GoodDollar.mint(swapMock.address, ethers.constants.WeiPerEther.mul(100000)); - await gdframework.GoodDollar.mint(deployer, ethers.constants.WeiPerEther.mul(100000)); - sfHost = sfFramework.host; + swapMock = (await deployments.get("SwapRouterMock")).address + const gd = await ethers.getContractAt("ISuperGoodDollar", (await deployments.get("GoodDollar")).address) + sfHost = await gd.getHost() + console.log("deployed test gd and sf host", gd.address, sfHost, swapMock) } else { const sfFramework = await Framework.create({ chainId: network.config.chainId || 0, @@ -58,7 +40,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const pool = await deploy('UBIPool', { // Learn more about args here: https://www.npmjs.com/package/hardhat-deploy#deploymentsdeploy from: deployer, - args: [sfHost, swapMock?.address || '0x5615CDAb10dc425a742d643d949a7F474C01abc4'], //uniswap on celo + args: [sfHost, swapMock || '0x5615CDAb10dc425a742d643d949a7F474C01abc4'], //uniswap on celo log: true, libraries: { HelperLibrary: helplib.address, diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 50d18d1e..288ba2e8 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@gooddollar/goodcollective-contracts", "packageManager": "yarn@3.2.1", - "version": "1.0.4", + "version": "1.1.0", "license": "MIT", "types": "./typechain-types/index.ts", "files": [ @@ -63,7 +63,7 @@ "test": "npx hardhat test", "test:coverage": "npx hardhat coverage", "deploy": "hardhat deploy --export-all ./releases/deployment.json", - "prepublish": "yarn version patch && yarn compile && git add package.json && git commit -m \"version bump\"", + "bump": "yarn version patch && yarn compile && git add package.json && git commit -m \"version bump\"", "publish": "yarn npm publish --access public", "test:setup": "yarn exec ./scripts/deployContracts.sh", "verify": "npx hardhat run scripts/verify.ts --network ${0}" diff --git a/packages/contracts/scripts/verify.ts b/packages/contracts/scripts/verify.ts index c15c5960..e12590f3 100644 --- a/packages/contracts/scripts/verify.ts +++ b/packages/contracts/scripts/verify.ts @@ -1,28 +1,88 @@ import hre, { ethers } from 'hardhat'; import fetch from 'node-fetch'; + +const verifyUbi = async () => { + const poolImpl = await hre.deployments.get('UBIPool'); + const contract = await hre.deployments.get('UBIPoolFactory'); + const factoryImpl = await hre.deployments.get('UBIPoolFactory_Implementation'); + + const factory = await ethers.getContractAt('UBIPoolFactory', contract.address); + + //verify beacon which is internal to the factory + const beacon = await factory.impl(); + console.log({ beacon, poolImpl: poolImpl.address, factoryImpl: factoryImpl.address, factory: contract.address }); + const verifyBecon = await hre.run('verify', { + address: beacon, + constructorArgsParams: [poolImpl.address], + contract: '@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol:UpgradeableBeacon', + }); + + // hardhat deployments plugin doesnt verify pool on etherscan + const verifyPool = await hre.run('verify', { + address: poolImpl.address, + constructorArgsParams: poolImpl.args, + }); + + await Promise.all([hre.run('sourcify'), hre.run('etherscan-verify')]); + + for (let c of [{ address: beacon }, poolImpl]) { + //copy beacon to sourcify + const res = await fetch('https://sourcify.dev/server/session/verify/etherscan', { + method: 'POST', + body: JSON.stringify({ address: c.address, chainId: String(hre.network.config.chainId) }), + headers: { + 'Content-Type': 'application/json', + }, + }); + console.log('sourcify copy result', c.address, res.statusText); + + } + + + + +} + const main = async () => { const contract = await hre.deployments.get('DirectPaymentsFactory'); const poolImpl = await hre.deployments.get('DirectPaymentsPool'); const factory = await ethers.getContractAt('DirectPaymentsFactory', contract.address); + const factoryImpl = await hre.deployments.get('DirectPaymentsFactory_Implementation'); + //verify beacon which is internal to the factory const beacon = await factory.impl(); - const verifyBecon = hre.run('verify', { + console.log({ beacon, poolImpl: poolImpl.address, factoryImpl: factoryImpl.address, factory: contract.address }); + + // verify beacon which is internal to the factory + const verifyBecon = await hre.run('verify', { address: beacon, constructorArgsParams: [poolImpl.address], contract: '@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol:UpgradeableBeacon', }); - await Promise.all([verifyBecon, hre.run('sourcify'), hre.run('etherscan-verify')]); - - //copy beacon to sourcify - const res = await fetch('https://sourcify.dev/server/session/verify/etherscan', { - method: 'POST', - body: JSON.stringify({ address: beacon, chainId: String(hre.network.config.chainId) }), - headers: { - 'Content-Type': 'application/json', - }, + + // hardhat deployments plugin doesnt verify pool on etherscan + const verifyPool = await hre.run('verify', { + address: poolImpl.address, + constructorArgsParams: poolImpl.args, }); - console.log('sourcify beacon copy result', res.statusText); + + await Promise.all([hre.run('sourcify'), hre.run('etherscan-verify')]); + + // copy manually verifed to sourcify + for (let c of [{ address: beacon }, poolImpl]) { + //copy beacon to sourcify + const res = await fetch('https://sourcify.dev/server/session/verify/etherscan', { + method: 'POST', + body: JSON.stringify({ address: c.address, chainId: String(hre.network.config.chainId) }), + headers: { + 'Content-Type': 'application/json', + }, + }); + console.log('sourcify copy result', c.address, res.statusText); + + } }; +verifyUbi().catch((e) => console.log(e)); main().catch((e) => console.log(e)); diff --git a/packages/contracts/test/DirectPayments/DirectPayments.claim.test.ts b/packages/contracts/test/DirectPayments/DirectPayments.claim.test.ts index 238b41b1..4a66d57e 100644 --- a/packages/contracts/test/DirectPayments/DirectPayments.claim.test.ts +++ b/packages/contracts/test/DirectPayments/DirectPayments.claim.test.ts @@ -2,7 +2,7 @@ import { deploySuperGoodDollar } from '@gooddollar/goodprotocol'; import { loadFixture, time } from '@nomicfoundation/hardhat-network-helpers'; import { deployTestFramework } from '@superfluid-finance/ethereum-contracts/dev-scripts/deploy-test-framework'; import { expect } from 'chai'; -import { DirectPaymentsPool, ProvableNFT } from 'typechain-types'; +import { DirectPaymentsFactory, DirectPaymentsPool, ProvableNFT } from 'typechain-types'; import { ethers, upgrades } from 'hardhat'; import { MockContract, deployMockContract } from 'ethereum-waffle'; @@ -65,6 +65,7 @@ describe('DirectPaymentsPool Claim', () => { }); const fixture = async () => { + const factory = await ethers.getContractFactory('ProvableNFT'); nft = (await upgrades.deployProxy(factory, ['nft', 'cc'], { kind: 'uups' })) as ProvableNFT; const helper = await ethers.deployContract('HelperLibrary'); @@ -74,24 +75,31 @@ describe('DirectPaymentsPool Claim', () => { membersValidator = await deployMockContract(signers[0], [ 'function isMemberValid(address pool,address operator,address member,bytes memory extraData) external returns (bool)', ]); + const poolImpl = await Pool.deploy(await gdframework.GoodDollar.getHost(), ethers.constants.AddressZero) + + const poolFactory = await upgrades.deployProxy(await ethers.getContractFactory("DirectPaymentsFactory"), [ + signer.address, + poolImpl.address, + nft.address, + ethers.constants.AddressZero, + 0 + ], { kind: 'uups' }) as DirectPaymentsFactory + + // console.log("deployed factory:", poolFactory.address) + await nft.grantRole(ethers.constants.HashZero, poolFactory.address) // all members are valid by default membersValidator.mock['isMemberValid'].returns(true); - pool = (await upgrades.deployProxy( - Pool, - [ - nft.address, - { ...poolSettings, membersValidator: membersValidator.address }, - poolLimits, - ethers.constants.AddressZero, - ], - { - unsafeAllowLinkedLibraries: true, - constructorArgs: [await gdframework.GoodDollar.getHost(), ethers.constants.AddressZero], - } - )) as DirectPaymentsPool; - await pool.deployed(); + + const poolTx = await (await poolFactory.createPool("xx", "ipfs", + { ...poolSettings, membersValidator: membersValidator.address }, + poolLimits, + )).wait() + // console.log("created pool:", poolTx.events) + const poolAddress = poolTx.events?.find(_ => _.event === "PoolCreated")?.args?.[0] + pool = Pool.attach(poolAddress) + const tx = await nft.mintPermissioned(signers[0].address, nftSample, true, []).then((_) => _.wait()); await gdframework.GoodDollar.mint(pool.address, ethers.constants.WeiPerEther.mul(100000)).then((_) => _.wait()); nftSampleId = tx.events?.find((e) => e.event === 'Transfer')?.args?.tokenId; diff --git a/packages/contracts/test/DirectPayments/DirectPayments.superapp.test.ts b/packages/contracts/test/DirectPayments/DirectPayments.superapp.test.ts index 562bcbed..f9cc9383 100644 --- a/packages/contracts/test/DirectPayments/DirectPayments.superapp.test.ts +++ b/packages/contracts/test/DirectPayments/DirectPayments.superapp.test.ts @@ -258,4 +258,37 @@ describe('DirectPaymentsPool Superapp', () => { expect(supporter.lastUpdated).gt(0); expect(supporter.flowRate).equal(Number(baseFlowRate)); }); + + it('should be able to swap mockToken and support single when 0 G$ balance in one tx + approve', async () => { + const signer = signers[1]; + + const mockToken = await (await ethers.getContractFactoryFromArtifact(ERC20ABI)).deploy('x', 'x'); + await mockToken.mint(signer.address, ethers.constants.WeiPerEther); + await (await mockToken.connect(signer).approve(pool.address, ethers.constants.WeiPerEther)).wait(); + + expect(await gdframework.GoodDollar.balanceOf(signer.address)).equal(0); + expect(await mockToken.balanceOf(signer.address)).gt(0); + + //mint to the swaprouter so it has G$s to send in exchange + await gdframework.GoodDollar.mint(await pool.swapRouter(), ethers.constants.WeiPerEther); + + const st = await sf.loadSuperToken(gdframework.GoodDollar.address); + const tx = await pool.connect(signer).supportWithSwap(signer.address, { + swapFrom: mockToken.address, + amount: ethers.constants.WeiPerEther, + minReturn: ethers.constants.WeiPerEther, + deadline: (Date.now() / 1000).toFixed(0), + path: '0x', + }, '0x') + + console.log((await tx.wait()).events) + + expect(await mockToken.balanceOf(signer.address)).eq(0); + expect(await gdframework.GoodDollar.balanceOf(signer.address)).eq(0); + + const supporter = await pool.supporters(signer.address); + expect(supporter.contribution).equal(ethers.constants.WeiPerEther); + expect(supporter.lastUpdated).eq(0); + expect(supporter.flowRate).equal(0); + }); });