From 0df797ed0cd9c23b36775532b2f2da246decfd33 Mon Sep 17 00:00:00 2001 From: 0xdavinchee <0xdavinchee@gmail.com> Date: Tue, 9 Jan 2024 12:45:25 +0200 Subject: [PATCH] [ETHEREUM-CONTRACTS] GDA (#1357) * SuperfluidUpgradeableBeaconTest test contract * adjustment fr * Update GeneralDistributionAgreement.t.sol * more testing cleanup * we'll do it LIVE! - deployments uncommented - test cleanups * formatting * wip cleanups + test removals - cleanup interface - add estimateDistributionActualAmount - remove a bunch of the gda demo specific tests * address some review comments * review comments cont. * fix build * should fix build + allow deploy * fix deployments - renames of supertokenpool => superfluidpool - fix deploy-framework * GDA wip - clean up functions - add isPatricianPeriod view functions - fix distributeFlow shifting order: adjust buffer happens after flow distribution - add basic liquidation test - increase coverage - make _assertGlobalInvariants virtual * flow nft cleanup/fixes - add metadata to ethereum-contracts - FlowNFT's do not take baseURI in constructor, it is a constant variable now, no longer in storage - SuperTokenFactory fixed to check existing canonical addresses against new contract canonical logic addresses - add validate nft deployments addresses script * workflow cleanups - add `permissions: write-all` to broken workflows - fix deploy-framework.js up - fix scripts => dev-scripts * fix up framework deployment - add more validation to script - proxiableUUID fixed for SuperfluidPool - deploy-framework fix * wip - add trusted forwarder test - fix info-print-contract-addresses * cleanup - optimization in distributeFlow - added more test cases * Pool NFTs WIP - IPoolAdminNFT, IPoolMemberNFT, IPoolNFTBase added - ISuperToken interface modified to include pool nft's - SuperToken and SuperTokenFactoryMock modified given updates to constructor - ConstantOutflowNFT: CFA or GDA - PoolAdminNFT, PoolMemberNFT and PoolNFTBase initial implementation - SuperToken constructor takes flow NFT proxies - SuperTokenFactory constructor takes flow NFT logic contracts - update code path also handles nft logic upgrades - SF deployment steps modified to deploy flow NFTs - deployment scripts modified: deploy-framework, info-print-addresses, verify-framework - other test files modified given new constructor - Fix up existing TS test suite * add erc20 to superfluidpool * pool nft integration - mint/burn pool member nft in superfluidpool: update member units - mint pool admin nft in gda: create pool * gda wip - inc/dec allowance - move IERC20 to ISuperfluidPool - add helpers + basic tests for approve/transfer - transfer WIP * validate + make test more robust * gda tests cleanup * fix verification - always use slotsBitmapLibraryAddress deployed for IDA for GDA * flow nft on distribute flow * testing cleanup * fix transfer test too * GDAv1 in SuperTokenV1Library - remove andAssert functions - use startPrank - clean up tests * full coverage * wip * Update deploy-framework.js * Update ISuperToken.sol * using lib for ISuperToken * add tests for lib - adding tests for library * gda wip - NFTs (Admin and Member) mint functions require passed pool to be registered - more function reuse in superfluidpool - add update function in pool member nft and call it in SuperfluidPool (similar to ConstantOutflowNFT.onUpdate) - use uint64 max flow rate in tests instead of int32 * fix build * function renaming + comments * gda harness cleanup - reduce code size of gdav1harness by combining function - fix up tests - add pool param to adjustBuffer - add superToken to superfluid pool events - new BufferAdjusted event in GDAv1 * gda subgraph hemingway bridge - gda schema added - getAbi.js cleaned up - manifest file updated - mapping skeleton added (not for aggregate yet) * gdav1 forwarder wip * mapping functions WIP - schema cleanup - event entities mapping implemented - hol entities mapping implemented * aggregate entities schema * - add docs for new properties - initialize new properties in getOrInit - add tests for new properties in matchstick * Delete hardhat.json * nft's wip - pool member nft try/catch - pool admin nft try/catch - make the to parameter an ISuperfluidPool type and fix expected types in tests and peripherals * add transfer units to pool test * nft testing wip - move `NoNFTSuperTokenMock` to `SuperTokenMock.sol` - pool NFT mocks and upgradability mock contracts created - PoolNFTBase integration tests includes storage layout tests * fix build * wip + refactoring tests - moved common NFT test code to ERC721.t.sol - removed `Proxy` suffix - added upgradability tests for PoolNFTs * fix build * testing wip - discovered some refactoring possibilities - added some helper functions - tested nft integration * tests refactor - refactored flow nft tests (constant inflow, constant outflow) - added tests for pool nft base * pool nft tests wip + cleanup * add assertions for nft's in gda helpers * add GDA to SDK-Core add gda and SuperfluidPool code to SDK-Core * TSDoc for SuperfluidPool * GDA in SDK-Core WIP - add GDA to SuperfluidLoader - deploy IDAv1Forwarder and GDAv1Forwarder in deploy-framework.js in local testing - add gdav1 to Framework initialization - add tsdoc to GeneralDistributionAgreementV1.ts - connect provider or signer to view contract calls - connect signer to write contract calls - add gda and superfluid pool functions to supertoken - gda tests wip * more tests and coverage WIP * wip - clean up forwarder - clean up deployer (remove duplicate functions) - add forwarder to deployer - add reusable function for deploying forwarders in deploy-framework - fix interfaces.ts for sdk-core * fix build + add tests * force artifact creation * undo force * final SDK-Core tests for GDA * make yarn dev use forge by default * refactor tests - move GDA helpers and asserts from GDAv1.t.sol to FoundrySuperfluidTester * Update package.json * tidying up * add total connected flow rate to pool * fix distribute tests - minor naming refactoring - uncomment _helperDistribute in randomPoolSeqs * add batch liquidator + toga to Framework struct * does not exist error * batch liquidator modified to handle GDA * fix code size too large * updateMemberUnits name change, _toSemanticMoneyUnit added, set TOGA as default reward address * toSemanticMoney * testing wip * fix build to unblock * fix build * Update SuperTokenV1Library.sol: make isMemberConnected internal * cleanup and wip - move nft code blocks into functions for easier reasoning in gda and pool - remove check for if flow exists before deletion - fix test case where lib deploy not needed - rename test contract - helper test functions cleanup + additions * gda test cleanup - remove old comments - remove _expectedPoolData - assert flow rate for member is correct * add assertions for distribute claimable * check actual flow rate - check distributor flow rate against actual flow rate - fix estimate flow distribution flow rate logic (adjustment flow case) * final helper assertion for distribute flow * fix build * subgraph progress * subgraph wip - MemberUpdated => MemberUnitsUpdated - gda aggregate entities mapping wip * superfluidpool mapping - finish aggregate mapping for superfluid pool * Update mappingHelpers.ts * subgraph tests wip - a bit unsure why the error is occurring in the subgraph tests... * fix subgraph tests - ordering of event params was wrong * clean up tests * add gda addresses * fix tests * fix deployment * contracts notes * tests cleanup * uncommented test * more tests + helpers added * more helpers and abstractions! - added some more subgraph tests - fix le build * fix lint:sol errors * fix echidna * warnings cleanup * break windows * add one more test * use supertokenv1library - get gda hot fuzz in * fixing warnings * fix semantic money * clean up build warnings * do not ignore success flag from low level call * disable deny_warnings for now * fix up build and lint * fix build * clean up echidna * cleanup - prefix with `_` for internal functions - fix `createPool` function issue - add `_addPool` function * fixes - assert global invariants in helpers - add otherAccounts/_listAccounts pattern to * fix some of the gda invariance issues * testing cleanup * add a few more actions * fix tests * Update getAbi.js * fix build * Update bundled-abi-contracts-list.json * GDA tooling (#1602) * gov scripts for governance update and transfer: make usable for prod governance * simplify gov init * ocd treatment: more consistent order of cof vs cif * nix setup hint * store version string in Resolver, fix contract change recognition for some factory related contracts * fix code changed detection for contracts the SuperTokenFactory depends on * new loader contracts on gda testnets * rev string shall always have 8 chars * store 16 chars of git revision * [SUPPLYCHAIN] Update lerna again after nx@16.7.3 (#1589) * [TOOLING] ops-scripts improvements for 1.7+ (#1586) * gov scripts for governance update and transfer: make usable for prod governance * simplify gov init * ocd treatment: more consistent order of cof vs cif * nix setup hint * store version string in Resolver, fix contract change recognition for some factory related contracts * rev string shall always have 8 chars * store 16 chars of git revision * Bump webpack in /sdk-redux-examples/sdk-redux-react-typecript (#1614) Bumps [webpack](https://github.com/webpack/webpack) from 5.75.0 to 5.88.2. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.75.0...v5.88.2) --- updated-dependencies: - dependency-name: webpack dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Miao ZhiCheng * [METADATA] 1594 | Add Subgraph endpoints to metadata (#1611) * add to metadata * update changelog --------- Co-authored-by: Miao ZhiCheng * Bump web3 from 1.10.0 to 1.10.1 (#1619) Bumps [web3](https://github.com/ChainSafe/web3.js) from 1.10.0 to 1.10.1. - [Release notes](https://github.com/ChainSafe/web3.js/releases) - [Changelog](https://github.com/web3/web3.js/blob/v1.10.1/CHANGELOG.md) - [Commits](https://github.com/ChainSafe/web3.js/compare/v1.10.0...v1.10.1) --- updated-dependencies: - dependency-name: web3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump http-cache-semantics (#1620) Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1. - [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1) --- updated-dependencies: - dependency-name: http-cache-semantics dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: Miao ZhiCheng Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 0xdavinchee <0xdavinchee@gmail.com> * gdav1 forwarder tested * fix tests * add required ISuperToken ABIs * include resolver/ISuperfluid abis * fix build * subgraph tests wip pool - handleFlowDistributionUpdated - handleInstantDistributionUpdated - handleDistributionClaimed - handleMemberUnitsUpdated - handlePoolConnectionUpdated poolMember - handleDistributionClaimed - handleMemberUnitsUpdated * pool distributor tests * more echidna actions * clean up hot fuzz + gda * add token actions add some more token actions * add TOGA as account * fix test cases * fix formatting * fix formatting * fix lint errors * add callAgreement wrappers * with ctx functions added * cleanup - reduce code size - create SolvencyHelperLibrary - finished supertokenv1library callback tests * add to event * fix gda address for avalanche-fuji * subgraph and sdk-core changes * wip - reduce code size - cleaned up deployers - schema additions - subgraph add userData - more cleanup incoming * deploy script for gda forwarder & addition to framework deploy script * added gdaV1Forwarder addresses for testnets * fix subgraph tests * deployment env * change file structure - move gda related contracts to `agreements/gdav1` folder - same thing with the interface files, to `interfaces/agreements/gdav1` * unnecessary suffering - do not use the harness pattern in the future, it was completely unnecessary => using .prop pattern suffices - code size optimization was unnecessary * cleanup - remove DelegatableTokenMonad - fix up test * fix hot fuzz breaking * Update bundled-abi-contracts-list.json * remove GDAv1Internal * Delete GDAv1Internal.sol * more memory for the job * rename test functions * explicitly set parallel to false * clean up build warnings * pass via env: * fix build * fixes attempt - move event entity creation to the end of handler - purposefully brick handleMemberUnitsUpdated to see if this is ever being hit * fix build * fix contract size issue * use correct sig + add gda address to config * gda address can be null * fix broken unit tests * fix flakey tests * more consistent naming of verification related vars * pool config - create pool with a poolConfig struct => this struct determines if unit transferability and distribution from any address is allowed - tests added to ensure the new configs actually block the actions - existing tests ensures that the configs don't break existing functionality - fix flakey test in FoundrySuperfluidTester in _addAccount - move SuperfluidPoolDeployerLibrary to gdaV1 folder * cleanup - comment cleanup - code cleanup * move pool config out of interface * move struct to interface * fix build * fix build attempt * fix build * [ETHEREUM-CONTRACTS] [GDA] Fixes for GDA (#1729) * [ETHEREUM-CONTRACTS] BatchLiquidator: don't revert for non-transferrable SuperTokens (#1707) * don't revert for non-transferrable SuperTokens * add test for custom tokens revert on transfer --------- Co-authored-by: Axe * patch getUnderlyingToken (#1718) * Bump undici from 5.21.0 to 5.26.3 (#1719) Bumps [undici](https://github.com/nodejs/undici) from 5.21.0 to 5.26.3. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v5.21.0...v5.26.3) --- updated-dependencies: - dependency-name: undici dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * add error hashes, use currentContext.timestamp * use getHost.getTimestamp() * Bump @babel/traverse from 7.21.3 to 7.23.2 (#1723) Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.21.3 to 7.23.2. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [SDK-CORE/ METADATA] SDK-Core No Governance Fix + Metadata Types (#1728) * cleanup * fix types * remove tests flakiness * remove forge.sh * [SDK-CORE] GoodDollar sdk core fix (#1734) * fix supertoken initialization for gooddollar * bump version + update changelog * use governance address from networkData * gooddollar symbol --------- Co-authored-by: Kaspar Kallas * [ETHEREUM-CONTRACTS] make deploy script compatible with ethers v6 (#1730) * make deploy script compatible with ethers v6 * more meaningful jsdoc * add new functions for dev-scripts deployment * update deploy.sh (#1738) * Bump browserify-sign in /packages/sdk-core/previous-versions-testing (#1740) Bumps [browserify-sign](https://github.com/crypto-browserify/browserify-sign) from 4.2.1 to 4.2.2. - [Changelog](https://github.com/browserify/browserify-sign/blob/main/CHANGELOG.md) - [Commits](https://github.com/crypto-browserify/browserify-sign/compare/v4.2.1...v4.2.2) --- updated-dependencies: - dependency-name: browserify-sign dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump browserify-sign from 4.2.1 to 4.2.2 (#1739) Bumps [browserify-sign](https://github.com/crypto-browserify/browserify-sign) from 4.2.1 to 4.2.2. - [Changelog](https://github.com/browserify/browserify-sign/blob/main/CHANGELOG.md) - [Commits](https://github.com/crypto-browserify/browserify-sign/compare/v4.2.1...v4.2.2) --- updated-dependencies: - dependency-name: browserify-sign dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix verification script * change GDA forwarder address (needed to redeploy) * change GDA forwarder address for polygon-mumbai * fix tests to fuzz with different pool configs and fix distributeFlow blocked liquidation issue * fix TOB-SUPERFLUID-2: Incorrect event emission in connectPool * fix TOB-SUPERFLUID-5: Large encoded buffer amount could manipulate preceding field * fix TOB-SUPERFLUID-6: Off-by-one gas left check * fix broken test * wrangle with reducing the code size of GDA... * cleanup * fix build + tests * cleanup * add update beacon proxy update paths * fix broken deploy script, add tests, add transfer ownership of beacon in deploy script * fix broken deploy script attempt 2 * [WORKFLOWS] Use nix in `handler.run-ethereum-contracts-script.yml` (#1745) * handler.run-ethereum-contracts-script.yml * add nix to handler.deploy-to-testnets.yml * [ETHEREUM-CONTRACTS] App credit test (#1743) * add app credit sanity test * cleanup console.logs * add cliName (#1748) * doConnect != isConnected fixed * remove extra whitespace * add assertEq in SFGovII test and import PoolConfig in ISuperfluid for easy access * fix build * fuzzing fix * EXPECT BREAKAGE IN FUZZ * fix build but echidna should break * undo breakage * fix the test * bump sdk-core version, fix sdk-core operation functions, fix subgraph mapping * fix unit tests * fix again * hot fuzz additions * [ETHEREUM-CONTRACTS] Fix canary build (#1742) * allow listing non-standard SuperTokens * fix canary build, fixes #1633 * distributeFlow: fix order of args to be consistent * fix build * add fix back in * [ETHEREUM-CONTRACTS] new resolver and loader, updated and bumped metadata, refs #1004 (#1750) * new resolver and loader, updated and bumped metadata, refs #1004 * updated changelog * gda version of loader * fix build * new resolver and loader, refs #1004 * remove getIsListed workaround assuming resolver exists (#1751) * fix broken unit test * fix gda logic * [ETHEREUM-CONTRACTS] new resolver & loader address for mainnets (#1752) * new resolver & loader address for mainnets * bumped metadata version * remove unimplemented function from yaml (#1753) * map the name from subgraph to unknown entity (#1754) * fix mapping * add total units * missing import * missing import pt 2 * [WORKFLOW] Subgraph deloy all networks (#1760) * added VENDOR_NETWORKS variable * decaled variable * fix mapping (#1758) * added new supported subgraphs (#1761) * remove duplicate verification * subgraph mapping addition * type fix (#1771) * readme fix --------- Signed-off-by: dependabot[bot] Co-authored-by: Didi Co-authored-by: Axe Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Kaspar Kallas Co-authored-by: Miao ZhiCheng Co-authored-by: Momodu Afegbua * bump versions * upload audit report * changes since audit file added --------- Signed-off-by: dependabot[bot] Co-authored-by: Axe Co-authored-by: Miao ZhiCheng Co-authored-by: Miao, ZhiCheng Co-authored-by: didi Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Kaspar Kallas Co-authored-by: Momodu Afegbua --- .github/workflows/call.deploy-dry-run.yml | 53 + .../call.test-ethereum-contracts.yml | 11 +- README.md | 12 +- .../autowrap/package.json | 2 +- .../scheduler/package.json | 2 +- packages/ethereum-contracts/CHANGELOG.md | 3 + ...- Comprehensive Report with Fix Review.pdf | Bin 0 -> 700363 bytes .../audits/changes-since-ToB-2023-audit.md | 13 + .../agreements/ConstantFlowAgreementV1.sol | 64 +- .../gdav1/GeneralDistributionAgreementV1.sol | 1096 +++++++++++++++++ .../agreements/gdav1/PoolAdminNFT.sol | 90 ++ .../agreements/gdav1/PoolMemberNFT.sol | 139 +++ .../agreements/gdav1/PoolNFTBase.sol | 279 +++++ .../agreements/gdav1/SuperfluidPool.sol | 488 ++++++++ .../gdav1/SuperfluidPoolDeployerLibrary.sol | 29 + .../contracts/apps/SuperTokenV1Library.sol | 420 ++++++- .../gov/SuperfluidGovernanceBase.sol | 6 +- .../gdav1/IGeneralDistributionAgreementV1.sol | 284 +++++ .../agreements/gdav1/IPoolAdminNFT.sol | 22 + .../agreements/gdav1/IPoolMemberNFT.sol | 37 + .../agreements/gdav1/IPoolNFTBase.sol | 28 + .../agreements/gdav1/ISuperfluidPool.sol | 122 ++ .../interfaces/superfluid/IPoolAdminNFT.sol | 6 - .../interfaces/superfluid/IPoolMemberNFT.sol | 6 - .../interfaces/superfluid/ISuperToken.sol | 22 + .../superfluid/ISuperTokenFactory.sol | 4 - .../interfaces/superfluid/ISuperfluid.sol | 19 +- .../superfluid/ISuperfluidGovernance.sol | 3 +- .../contracts/libs/SafeGasLibrary.sol | 2 +- .../contracts/libs/SolvencyHelperLibrary.sol | 39 + .../contracts/mocks/CFAv1NFTMock.sol | 222 +--- .../mocks/CFAv1NFTUpgradabilityMock.sol | 45 +- .../contracts/mocks/IStorageLayoutBase.sol | 6 + .../contracts/mocks/PoolNFTMock.sol | 95 ++ .../mocks/PoolNFTUpgradabilityMock.sol | 152 +++ .../contracts/mocks/SuperTokenFactoryMock.sol | 68 +- .../mocks/SuperTokenLibraryV1Mock.sol | 158 ++- .../contracts/mocks/SuperTokenMock.sol | 156 ++- .../mocks/SuperfluidPoolUpgradabilityMock.sol | 52 + .../superfluid/ConstantInflowNFT.sol | 34 +- .../superfluid/ConstantOutflowNFT.sol | 86 +- .../contracts/superfluid/FlowNFTBase.sol | 189 +-- .../contracts/superfluid/SuperToken.sol | 19 +- .../superfluid/SuperTokenFactory.sol | 38 +- .../contracts/superfluid/Superfluid.sol | 20 +- .../upgradability/BeaconProxiable.sol | 17 + .../SuperfluidUpgradeableBeacon.sol | 31 + .../contracts/upgradability/UUPSProxiable.sol | 2 +- .../contracts/utils/BatchLiquidator.sol | 128 +- .../contracts/utils/GDAv1Forwarder.sol | 243 ++++ .../utils/SuperfluidFrameworkDeployer.sol | 197 +-- .../SuperfluidFrameworkDeploymentSteps.sol | 556 +++++---- .../contracts/utils/SuperfluidLoader.sol | 4 + .../dev-scripts/deploy-contracts-and-token.js | 5 +- .../dev-scripts/deploy-test-framework.js | 69 +- .../run-deploy-contracts-and-token.js | 3 +- packages/ethereum-contracts/foundry.toml | 1 + .../ops-scripts/deploy-aux-contracts.js | 10 +- .../ops-scripts/deploy-deterministically.js | 11 +- .../ops-scripts/deploy-framework.js | 629 ++++++++-- .../info-print-contract-addresses.js | 60 +- .../ops-scripts/libs/common.js | 1 + .../ops-scripts/libs/getConfig.js | 1 + .../ops-scripts/validate-deployment.ts | 143 +++ .../ops-scripts/validate-nft-addresses.ts | 49 - packages/ethereum-contracts/package.json | 4 +- .../tasks/bundled-abi-contracts-list.json | 5 +- .../tasks/coverage-cleanup.sh | 4 +- .../tasks/deploy-cfa-forwarder.sh | 40 +- .../tasks/deploy-gda-forwarder.sh | 54 + .../tasks/etherscan-verify-framework.sh | 62 +- .../tasks/etherscan-verify-proxies.sh | 4 +- .../test/TestEnvironment.ts | 89 +- .../apps/SuperTokenV1Library.CFA.test.ts | 12 +- .../apps/SuperTokenV1Library.GDA.test.ts | 446 +++++++ .../gov/SuperfluidGovernanceII.test.ts | 1 + .../superfluid/SuperToken.NonStandard.test.ts | 24 +- .../superfluid/SuperTokenFactory.test.ts | 54 +- .../contracts/superfluid/Superfluid.test.ts | 51 +- .../test/foundry/FoundrySuperfluidTester.sol | 850 ++++++++++++- .../foundry/SuperfluidFrameworkDeployer.t.sol | 6 +- .../gdav1/GeneralDistributionAgreement.t.sol | 933 ++++++++++++++ .../GeneralDistributionAgreementV1.prop.sol | 372 ++++++ .../foundry/apps/CrossStreamSuperApp.t.sol | 7 + .../foundry/echidna/EchidnaTestCases.t.sol | 44 + .../foundry/gov/SuperfluidGovernanceII.t.sol | 49 +- .../foundry/libs/SlotsBitmapLibrary.prop.sol | 2 +- .../superfluid/ConstantInflowNFT.t.sol | 169 +-- .../superfluid/ConstantOutflowNFT.t.sol | 217 +--- .../test/foundry/superfluid/ERC721.t.sol | 278 +++++ .../test/foundry/superfluid/FlowNFTBase.t.sol | 395 +++--- .../foundry/superfluid/PoolAdminNFT.t.sol | 54 + .../foundry/superfluid/PoolMemberNFT.t.sol | 84 ++ .../test/foundry/superfluid/PoolNFTBase.t.sol | 408 ++++++ .../test/foundry/superfluid/SuperToken.t.sol | 70 +- .../superfluid/SuperTokenFactory.t.sol | 12 +- .../test/foundry/superfluid/Superfluid.t.sol | 11 +- .../superfluid/SuperfluidPool.prop.sol | 102 ++ .../SuperfluidUpgradeableBeacon.t.sol | 70 ++ .../test/foundry/utils/BatchLiquidator.t.sol | 361 ++++-- .../test/foundry/utils/TOGA.t.sol | 3 +- .../test/ops-scripts/deployment.test.js | 1 + packages/ethereum-contracts/test/types.ts | 2 + .../testsuites/apps-contracts.ts | 1 + packages/hot-fuzz/README.md | 4 +- packages/hot-fuzz/contracts/HotFuzzBase.sol | 68 +- .../hot-fuzz/contracts/SuperfluidTester.sol | 180 ++- .../ConstantFlowAgreementV1.hott.sol | 95 +- .../superfluid-tests/GDAHotFuzz.yaml | 1 + .../GeneralDistributionAgreementV1.hott.sol | 155 +++ .../InstantDistributionAgreementV1.hott.sol | 43 +- .../superfluid-tests/SuperHotFuzz.sol | 8 +- .../superfluid-tests/SuperToken.hott.sol | 46 +- packages/hot-fuzz/echidna.yaml | 31 +- packages/hot-fuzz/package.json | 2 +- packages/js-sdk/package.json | 2 +- packages/metadata/main/networks/list.cjs | 7 +- packages/metadata/module/networks/list.js | 7 +- packages/metadata/networks.json | 7 +- packages/sdk-core/CHANGELOG.md | 1 - packages/sdk-core/package.json | 4 +- .../sdk-core/src/ConstantFlowAgreementV1.ts | 24 +- packages/sdk-core/src/Framework.ts | 20 + .../src/GeneralDistributionAgreementV1.ts | 517 ++++++++ .../src/InstantDistributionAgreementV1.ts | 4 +- packages/sdk-core/src/SFError.ts | 6 + packages/sdk-core/src/SuperToken.ts | 228 ++++ packages/sdk-core/src/SuperfluidAgreement.ts | 25 + packages/sdk-core/src/SuperfluidPool.ts | 478 +++++++ packages/sdk-core/src/index.ts | 4 + packages/sdk-core/src/interfaces.ts | 213 +++- .../sdk-core/test/1.4_supertoken_gda.test.ts | 1061 ++++++++++++++++ packages/sdk-core/test/4_governance.test.ts | 14 +- packages/sdk-core/test/TestEnvironment.ts | 10 +- .../src/ref-impl/ISuperfluidPool.sol | 2 +- packages/subgraph/.prettierrc.js | 1 + packages/subgraph/config/mock.json | 1 + packages/subgraph/package.json | 5 +- packages/subgraph/schema.graphql | 802 +++++++++++- .../subgraph/scripts/buildNetworkConfig.ts | 2 + packages/subgraph/scripts/getAbi.js | 11 +- packages/subgraph/src/mappingHelpers.ts | 731 +++++++++-- packages/subgraph/src/mappings/cfav1.ts | 43 +- packages/subgraph/src/mappings/gdav1.ts | 490 ++++++++ packages/subgraph/src/mappings/idav1.ts | 30 +- .../subgraph/src/mappings/superfluidPool.ts | 162 +++ packages/subgraph/src/utils.ts | 83 +- packages/subgraph/subgraph.template.yaml | 96 +- packages/subgraph/tests/assertionHelpers.ts | 156 ++- .../tests/cfav1/hol/cfav1.hol.test.ts | 48 +- packages/subgraph/tests/constants.ts | 8 +- .../tests/gdav1/event/gdav1.event.test.ts | 260 ++++ packages/subgraph/tests/gdav1/gdav1.helper.ts | 227 ++++ .../tests/gdav1/hol/gdav1.hol.test.ts | 693 +++++++++++ .../subgraph/tests/resolver/resolver.test.ts | 67 +- .../superToken/event/superToken.event.test.ts | 79 +- .../superToken/hol/supertoken.hol.test.ts | 48 +- .../superTokenFactory.test.ts | 19 +- yarn.lock | 87 +- 159 files changed, 17076 insertions(+), 2492 deletions(-) create mode 100644 .github/workflows/call.deploy-dry-run.yml create mode 100644 packages/ethereum-contracts/audits/Superfluid - Finance GDA - Comprehensive Report with Fix Review.pdf create mode 100644 packages/ethereum-contracts/audits/changes-since-ToB-2023-audit.md create mode 100644 packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol create mode 100644 packages/ethereum-contracts/contracts/agreements/gdav1/PoolAdminNFT.sol create mode 100644 packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol create mode 100644 packages/ethereum-contracts/contracts/agreements/gdav1/PoolNFTBase.sol create mode 100644 packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol create mode 100644 packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol create mode 100644 packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol create mode 100644 packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolAdminNFT.sol create mode 100644 packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol create mode 100644 packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolNFTBase.sol create mode 100644 packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol delete mode 100644 packages/ethereum-contracts/contracts/interfaces/superfluid/IPoolAdminNFT.sol delete mode 100644 packages/ethereum-contracts/contracts/interfaces/superfluid/IPoolMemberNFT.sol create mode 100644 packages/ethereum-contracts/contracts/libs/SolvencyHelperLibrary.sol create mode 100644 packages/ethereum-contracts/contracts/mocks/IStorageLayoutBase.sol create mode 100644 packages/ethereum-contracts/contracts/mocks/PoolNFTMock.sol create mode 100644 packages/ethereum-contracts/contracts/mocks/PoolNFTUpgradabilityMock.sol create mode 100644 packages/ethereum-contracts/contracts/mocks/SuperfluidPoolUpgradabilityMock.sol create mode 100644 packages/ethereum-contracts/contracts/upgradability/BeaconProxiable.sol create mode 100644 packages/ethereum-contracts/contracts/upgradability/SuperfluidUpgradeableBeacon.sol create mode 100644 packages/ethereum-contracts/contracts/utils/GDAv1Forwarder.sol create mode 100644 packages/ethereum-contracts/ops-scripts/validate-deployment.ts delete mode 100644 packages/ethereum-contracts/ops-scripts/validate-nft-addresses.ts create mode 100755 packages/ethereum-contracts/tasks/deploy-gda-forwarder.sh create mode 100644 packages/ethereum-contracts/test/contracts/apps/SuperTokenV1Library.GDA.test.ts create mode 100644 packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol create mode 100644 packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreementV1.prop.sol create mode 100644 packages/ethereum-contracts/test/foundry/echidna/EchidnaTestCases.t.sol create mode 100644 packages/ethereum-contracts/test/foundry/superfluid/ERC721.t.sol create mode 100644 packages/ethereum-contracts/test/foundry/superfluid/PoolAdminNFT.t.sol create mode 100644 packages/ethereum-contracts/test/foundry/superfluid/PoolMemberNFT.t.sol create mode 100644 packages/ethereum-contracts/test/foundry/superfluid/PoolNFTBase.t.sol create mode 100644 packages/ethereum-contracts/test/foundry/superfluid/SuperfluidPool.prop.sol create mode 100644 packages/ethereum-contracts/test/foundry/upgradability/SuperfluidUpgradeableBeacon.t.sol create mode 100644 packages/hot-fuzz/contracts/superfluid-tests/GDAHotFuzz.yaml create mode 100644 packages/hot-fuzz/contracts/superfluid-tests/GeneralDistributionAgreementV1.hott.sol create mode 100644 packages/sdk-core/src/GeneralDistributionAgreementV1.ts create mode 100644 packages/sdk-core/src/SuperfluidAgreement.ts create mode 100644 packages/sdk-core/src/SuperfluidPool.ts create mode 100644 packages/sdk-core/test/1.4_supertoken_gda.test.ts create mode 100644 packages/subgraph/src/mappings/gdav1.ts create mode 100644 packages/subgraph/src/mappings/superfluidPool.ts create mode 100644 packages/subgraph/tests/gdav1/event/gdav1.event.test.ts create mode 100644 packages/subgraph/tests/gdav1/gdav1.helper.ts create mode 100644 packages/subgraph/tests/gdav1/hol/gdav1.hol.test.ts diff --git a/.github/workflows/call.deploy-dry-run.yml b/.github/workflows/call.deploy-dry-run.yml new file mode 100644 index 0000000000..0ec83e44b7 --- /dev/null +++ b/.github/workflows/call.deploy-dry-run.yml @@ -0,0 +1,53 @@ +name: Reusable Workflow | Deploy Framework and Update Tokens on a Forked Network + +on: + workflow_call: + inputs: + network: + required: true + type: string + network-id: + required: true + type: string + provider-url: + required: true + type: string + +jobs: + deploy-to-forked-network: + name: Deploy Framework and Update Tokens on a Forked Network + runs-on: ubuntu-latest + env: + ethereum-contracts-working-directory: ./packages/ethereum-contracts + + steps: + - uses: actions/checkout@v3 + + - name: Install and Build + run: | + yarn install --frozen-lockfile + yarn build-for-contracts-dev + + - name: Start ganache + run: npx ganache --port 47545 --mnemonic --fork.url ${{ github.event.inputs.provider-url }} --network-id ${{ github.event.inputs.network-id }} --chain.chainId ${{ github.event.inputs.network-id }} + + - name: Deploy framework + run: | + echo "${{ github.event.inputs.environments }}" | sed 's/;/\n/' > .env + npx truffle exec --network ${{ github.event.inputs.network }} ops-scripts/deploy-test-environment.js + working-directory: ${{ env.ethereum-contracts-working-directory }} + + - name: Validate deployment before token upgrade + run: | + npx hardhat run ops-scripts/validate-deployment.ts --network ${{ github.event.inputs.network }} + working-directory: ${{ env.ethereum-contracts-working-directory }} + + - name: Update Super Token Logic for all tokens + run: | + npx truffle exec --network ${{ github.event.inputs.network }} ops-scripts/gov-upgrade-super-token-logic.js : ALL + working-directory: ${{ env.ethereum-contracts-working-directory }} + + - name: Validate deployment post token upgrade + run: | + npx hardhat run ops-scripts/validate-deployment.ts --network ${{ github.event.inputs.network }} + working-directory: ${{ env.ethereum-contracts-working-directory }} diff --git a/.github/workflows/call.test-ethereum-contracts.yml b/.github/workflows/call.test-ethereum-contracts.yml index 095201cf08..c16f36276e 100644 --- a/.github/workflows/call.test-ethereum-contracts.yml +++ b/.github/workflows/call.test-ethereum-contracts.yml @@ -99,11 +99,12 @@ jobs: # # Upstream issue: https://github.com/NomicFoundation/hardhat/issues/4310 # Though more likely, it is an issue to https://github.com/sc-forks/solidity-coverage - # env: - # # NOTE: 4 workers would overwhelm the free-tier github runner - # IS_COVERAGE_TEST: true - # HARDHAT_TEST_JOBS: 2 - # HARDHAT_RUN_PARALLEL: true + env: + # NOTE: 4 workers would overwhelm the free-tier github runner + NODE_OPTIONS: --max_old_space_size=4096 + IS_COVERAGE_TEST: true + HARDHAT_TEST_JOBS: 2 + HARDHAT_RUN_PARALLEL: false - name: Clean up and merge coverage artifacts if: inputs.run-coverage-tests == true diff --git a/README.md b/README.md index 8cff7b072d..0b81b93adb 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,22 @@

Welcome to superfluid protocol-monorepo 👋

- + npm - + GitHub package.json version (subfolder of monorepo) - + npm - + npm - + npm - + GitHub package.json version (subfolder of monorepo)
diff --git a/packages/automation-contracts/autowrap/package.json b/packages/automation-contracts/autowrap/package.json index b69b4f5320..4d37366bfd 100644 --- a/packages/automation-contracts/autowrap/package.json +++ b/packages/automation-contracts/autowrap/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@openzeppelin/contracts": "4.9.3", - "@superfluid-finance/ethereum-contracts": "1.8.1", + "@superfluid-finance/ethereum-contracts": "1.9.0", "@superfluid-finance/metadata": "1.1.22" } } diff --git a/packages/automation-contracts/scheduler/package.json b/packages/automation-contracts/scheduler/package.json index 542be34118..13d6c91cf9 100644 --- a/packages/automation-contracts/scheduler/package.json +++ b/packages/automation-contracts/scheduler/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@openzeppelin/contracts": "4.9.3", - "@superfluid-finance/ethereum-contracts": "1.8.1", + "@superfluid-finance/ethereum-contracts": "1.9.0", "@superfluid-finance/metadata": "1.1.22" } } diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index 1e022c7bd3..23338ed5d5 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -5,6 +5,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## Unreleased +## [v1.9.0] - 2024-01-09 + ### Breaking - `TokenInfo` and `ERC20WithTokenInfo` interface/abstract contract are removed from the codebase, including the bundled ABI contracts @@ -14,6 +16,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added +- New agreement: `GeneralDistributionAgreement` added which enables 1-to-N flowing distributions in addition to 1-to-N instant distributions via the `SuperfluidPool` contract - Added 'test-slither' yarn sub-task. - Expose `SuperToken._underlyingDecimals` with `SuperToken.getUnderlyingDecimals()` - Expose `_toUnderlyingAmount(uint256 amount)` with `toUnderlyingAmount(uint256 amount)` diff --git a/packages/ethereum-contracts/audits/Superfluid - Finance GDA - Comprehensive Report with Fix Review.pdf b/packages/ethereum-contracts/audits/Superfluid - Finance GDA - Comprehensive Report with Fix Review.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0cd80588ff1486adb0e9822e4cd25f2d56df1aad GIT binary patch literal 700363 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

  • _ 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

    h_`!#YbiBv`_tvwpP}r3>cZXUl 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 UPWA3PXdN%FZ(!ikt1s*OH}>m2H~;_u literal 0 HcmV?d00001 diff --git a/packages/ethereum-contracts/audits/changes-since-ToB-2023-audit.md b/packages/ethereum-contracts/audits/changes-since-ToB-2023-audit.md new file mode 100644 index 0000000000..430ef2d5e0 --- /dev/null +++ b/packages/ethereum-contracts/audits/changes-since-ToB-2023-audit.md @@ -0,0 +1,13 @@ +# Changes Since ToB 2023 Audit + +Use `git diff 4ece1a3f4aff8b5a9cbf37118d261023960c0f0f.. packages/ethereum-contracts/contracts` to see the changes in the contract code since the audit commit hash. + +## High Level Summary of Changes + +### GeneralDistributionAgreementV1 +- The representation of totalBuffer is modified to ensure proper data fitting in a 256-bit field. +- `realtimeBalanceVectorAt` removed +- `PoolConnectionUpdated` event only emitted if the connection was changed + +### SuperfluidPool +The method for obtaining timestamps and checking member connections is updated to use Superfluid framework methods instead of Ethereum's native functionalities. \ No newline at end of file diff --git a/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol index 1586b332f4..d383e8ca97 100644 --- a/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol @@ -18,6 +18,7 @@ import { AgreementBase } from "./AgreementBase.sol"; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { AgreementLibrary } from "./AgreementLibrary.sol"; import { SafeGasLibrary } from "../libs/SafeGasLibrary.sol"; +import { SolvencyHelperLibrary } from "../libs/SolvencyHelperLibrary.sol"; /** * @title ConstantFlowAgreementV1 contract @@ -164,7 +165,7 @@ contract ConstantFlowAgreementV1 is external view override returns (int96 flowRate) { - (uint256 liquidationPeriod, ) = _decode3PsData(token); + (uint256 liquidationPeriod, ) = SolvencyHelperLibrary.decode3PsData(ISuperfluid(_host), token); flowRate = _getMaximumFlowRateFromDepositPure(liquidationPeriod, deposit); } @@ -209,11 +210,12 @@ contract ConstantFlowAgreementV1 is return true; } - (uint256 liquidationPeriod, uint256 patricianPeriod) = _decode3PsData(token); + (uint256 liquidationPeriod, uint256 patricianPeriod) = + SolvencyHelperLibrary.decode3PsData(ISuperfluid(_host), token); (,FlowData memory senderAccountState) = _getAccountFlowState(token, account); int256 signedTotalCFADeposit = senderAccountState.deposit.toInt256(); - return _isPatricianPeriod( + return SolvencyHelperLibrary.isPatricianPeriod( availableBalance, signedTotalCFADeposit, liquidationPeriod, @@ -1345,7 +1347,9 @@ contract ConstantFlowAgreementV1 is uint256 minimumDeposit; // STEP 1: calculate deposit required for the flow { - (uint256 liquidationPeriod, ) = _decode3PsData(token); + + (uint256 liquidationPeriod,) = + SolvencyHelperLibrary.decode3PsData(ISuperfluid(_host), token); ISuperfluidGovernance gov = ISuperfluidGovernance(ISuperfluid(msg.sender).getGovernance()); minimumDeposit = gov.getConfigAsUint256( ISuperfluid(msg.sender), token, SuperfluidGovernanceConfigs.SUPERTOKEN_MINIMUM_DEPOSIT_KEY); @@ -1466,7 +1470,7 @@ contract ConstantFlowAgreementV1 is (,FlowData memory senderAccountState) = _getAccountFlowState(token, flowParams.sender); int256 signedSingleDeposit = flowData.deposit.toInt256(); - // TODO: GDA deposit should be considered here too + int256 signedTotalCFADeposit = senderAccountState.deposit.toInt256(); bytes memory liquidationTypeData; bool isCurrentlyPatricianPeriod; @@ -1482,8 +1486,9 @@ contract ConstantFlowAgreementV1 is // To retrieve patrician period // Note: curly brackets are to handle stack too deep overflow issue { - (uint256 liquidationPeriod, uint256 patricianPeriod) = _decode3PsData(token); - isCurrentlyPatricianPeriod = _isPatricianPeriod( + (uint256 liquidationPeriod, uint256 patricianPeriod) = + SolvencyHelperLibrary.decode3PsData(ISuperfluid(_host), token); + isCurrentlyPatricianPeriod = SolvencyHelperLibrary.isPatricianPeriod( availableBalance, signedTotalCFADeposit, liquidationPeriod, @@ -1616,51 +1621,6 @@ contract ConstantFlowAgreementV1 is } } - /************************************************************************** - * 3P's Pure Functions - *************************************************************************/ - - // - // Data packing: - // - // WORD A: | reserved | patricianPeriod | liquidationPeriod | - // | 192 | 32 | 32 | - // - // NOTE: - // - liquidation period has 32 bits length - // - patrician period also has 32 bits length - - function _decode3PsData( - ISuperfluidToken token - ) - internal view - returns(uint256 liquidationPeriod, uint256 patricianPeriod) - { - ISuperfluidGovernance gov = ISuperfluidGovernance(ISuperfluid(_host).getGovernance()); - uint256 pppConfig = - gov.getConfigAsUint256(ISuperfluid(_host), token, SuperfluidGovernanceConfigs.CFAV1_PPP_CONFIG_KEY); - (liquidationPeriod, patricianPeriod) = SuperfluidGovernanceConfigs.decodePPPConfig(pppConfig); - } - - function _isPatricianPeriod( - int256 availableBalance, - int256 signedTotalCFADeposit, - uint256 liquidationPeriod, - uint256 patricianPeriod - ) - internal pure - returns (bool) - { - if (signedTotalCFADeposit == 0) { - return false; - } - - int256 totalRewardLeft = availableBalance + signedTotalCFADeposit; - int256 totalCFAOutFlowrate = signedTotalCFADeposit / int256(liquidationPeriod); - // divisor cannot be zero with existing outflow - return totalRewardLeft / totalCFAOutFlowrate > int256(liquidationPeriod - patricianPeriod); - } - /************************************************************************** * ACL Pure Functions *************************************************************************/ diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol new file mode 100644 index 0000000000..19be6f0955 --- /dev/null +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -0,0 +1,1096 @@ +// SPDX-License-Identifier: AGPLv3 +// solhint-disable not-rely-on-time +pragma solidity 0.8.19; + +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { IBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; + +import { ISuperfluid, ISuperfluidGovernance } from "../../interfaces/superfluid/ISuperfluid.sol"; +import { + BasicParticle, + PDPoolIndex, + SemanticMoney, + Value, + Time, + FlowRate +} from "@superfluid-finance/solidity-semantic-money/src/SemanticMoney.sol"; +import { TokenMonad } from "@superfluid-finance/solidity-semantic-money/src/TokenMonad.sol"; +import { SuperfluidPool } from "./SuperfluidPool.sol"; +import { SuperfluidPoolDeployerLibrary } from "./SuperfluidPoolDeployerLibrary.sol"; +import { + IGeneralDistributionAgreementV1, + PoolConfig +} from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; +import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol"; +import { IConstantOutflowNFT } from "../../interfaces/superfluid/IConstantOutflowNFT.sol"; +import { ISuperToken } from "../../interfaces/superfluid/ISuperToken.sol"; +import { IPoolAdminNFT } from "../../interfaces/agreements/gdav1/IPoolAdminNFT.sol"; +import { ISuperfluidPool } from "../../interfaces/agreements/gdav1/ISuperfluidPool.sol"; +import { SlotsBitmapLibrary } from "../../libs/SlotsBitmapLibrary.sol"; +import { SolvencyHelperLibrary } from "../../libs/SolvencyHelperLibrary.sol"; +import { SafeGasLibrary } from "../../libs/SafeGasLibrary.sol"; +import { AgreementBase } from "../AgreementBase.sol"; +import { AgreementLibrary } from "../AgreementLibrary.sol"; + +/** + * @title General Distribution Agreement + * @author Superfluid + * @notice + * + * Storage Layout Notes + * Agreement State + * + * Universal Index Data + * slotId = _UNIVERSAL_INDEX_STATE_SLOT_ID or 0 + * msg.sender = address of GDAv1 + * account = context.msgSender + * Universal Index Data stores a Basic Particle for an account as well as the total buffer and + * whether the account is a pool or not. + * + * SlotsBitmap Data + * slotId = _POOL_SUBS_BITMAP_STATE_SLOT_ID or 1 + * msg.sender = address of GDAv1 + * account = context.msgSender + * Slots Bitmap Data Slot stores a bitmap of the slots that are "enabled" for a pool member. + * + * Pool Connections Data Slot Id Start + * slotId (start) = _POOL_CONNECTIONS_DATA_STATE_SLOT_ID_START or 1 << 128 or 340282366920938463463374607431768211456 + * msg.sender = address of GDAv1 + * account = context.msgSender + * Pool Connections Data Slot Id Start indicates the starting slot for where we begin to store the pools that a + * pool member is a part of. + * + * + * Agreement Data + * NOTE The Agreement Data slot is calculated with the following function: + * keccak256(abi.encode("AgreementData", agreementClass, agreementId)) + * agreementClass = address of GDAv1 + * agreementId = DistributionFlowId | PoolMemberId + * + * DistributionFlowId = + * keccak256(abi.encode(block.chainid, "distributionFlow", from, pool)) + * DistributionFlowId stores FlowDistributionData between a sender (from) and pool. + * + * PoolMemberId = + * keccak256(abi.encode(block.chainid, "poolMember", member, pool)) + * PoolMemberId stores PoolMemberData for a member at a pool. + */ +contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDistributionAgreementV1 { + using SafeCast for uint256; + using SafeCast for int256; + using SemanticMoney for BasicParticle; + + address public constant SLOTS_BITMAP_LIBRARY_ADDRESS = address(SlotsBitmapLibrary); + + address public constant SUPERFLUID_POOL_DEPLOYER_ADDRESS = address(SuperfluidPoolDeployerLibrary); + + /// @dev Universal Index state slot id for storing universal index data + uint256 private constant _UNIVERSAL_INDEX_STATE_SLOT_ID = 0; + /// @dev Pool member state slot id for storing subs bitmap + uint256 private constant _POOL_SUBS_BITMAP_STATE_SLOT_ID = 1; + /// @dev Pool member state slot id starting point for pool connections + uint256 private constant _POOL_CONNECTIONS_DATA_STATE_SLOT_ID_START = 1 << 128; + /// @dev SuperToken minimum deposit key + bytes32 private constant SUPERTOKEN_MINIMUM_DEPOSIT_KEY = + keccak256("org.superfluid-finance.superfluid.superTokenMinimumDeposit"); + + IBeacon public superfluidPoolBeacon; + + constructor(ISuperfluid host) AgreementBase(address(host)) { } + + function initialize(IBeacon superfluidPoolBeacon_) external initializer { + superfluidPoolBeacon = superfluidPoolBeacon_; + } + + function realtimeBalanceOf(ISuperfluidToken token, address account, uint256 time) + public + view + override + returns (int256 rtb, uint256 buf, uint256 owedBuffer) + { + UniversalIndexData memory universalIndexData = _getUIndexData(abi.encode(token), account); + + if (_isPool(token, account)) { + rtb = ISuperfluidPool(account).getDisconnectedBalance(uint32(time)); + } else { + rtb = Value.unwrap(_getBasicParticleFromUIndex(universalIndexData).rtb(Time.wrap(uint32(time)))); + } + + int256 fromPools; + { + (uint32[] memory slotIds, bytes32[] memory pidList) = _listPoolConnectionIds(token, account); + for (uint256 i = 0; i < slotIds.length; ++i) { + address pool = address(uint160(uint256(pidList[i]))); + (bool exist, PoolMemberData memory poolMemberData) = + _getPoolMemberData(token, account, ISuperfluidPool(pool)); + assert(exist); + assert(poolMemberData.pool == pool); + fromPools += ISuperfluidPool(pool).getClaimable(account, uint32(time)); + } + } + rtb += fromPools; + + buf = uint256(universalIndexData.totalBuffer.toInt256()); // upcasting to uint256 is safe + } + + /// @dev ISuperAgreement.realtimeBalanceOf implementation + function realtimeBalanceOfNow(ISuperfluidToken token, address account) + external + view + returns (int256 availableBalance, uint256 buffer, uint256 owedBuffer, uint256 timestamp) + { + (availableBalance, buffer, owedBuffer) = realtimeBalanceOf(token, account, block.timestamp); + timestamp = block.timestamp; + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function getNetFlow(ISuperfluidToken token, address account) external view override returns (int96 netFlowRate) { + netFlowRate = int256(FlowRate.unwrap(_getUIndex(abi.encode(token), account).flow_rate())).toInt96(); + + if (_isPool(token, account)) { + netFlowRate += ISuperfluidPool(account).getTotalDisconnectedFlowRate(); + } + + { + (uint32[] memory slotIds, bytes32[] memory pidList) = _listPoolConnectionIds(token, account); + for (uint256 i = 0; i < slotIds.length; ++i) { + ISuperfluidPool pool = ISuperfluidPool(address(uint160(uint256(pidList[i])))); + netFlowRate += pool.getMemberFlowRate(account); + } + } + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function getFlowRate(ISuperfluidToken token, address from, ISuperfluidPool to) + external + view + override + returns (int96) + { + (, FlowDistributionData memory data) = _getFlowDistributionData(token, _getFlowDistributionHash(from, to)); + return data.flowRate; + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function estimateFlowDistributionActualFlowRate( + ISuperfluidToken token, + address from, + ISuperfluidPool to, + int96 requestedFlowRate + ) external view override returns (int96 actualFlowRate, int96 totalDistributionFlowRate) { + bytes memory eff = abi.encode(token); + bytes32 distributionFlowHash = _getFlowDistributionHash(from, to); + + BasicParticle memory fromUIndexData = _getUIndex(eff, from); + + PDPoolIndex memory pdpIndex = _getPDPIndex("", address(to)); + + FlowRate oldFlowRate = _getFlowRate(eff, distributionFlowHash); + FlowRate newActualFlowRate; + FlowRate oldDistributionFlowRate = pdpIndex.flow_rate(); + FlowRate newDistributionFlowRate; + FlowRate flowRateDelta = FlowRate.wrap(requestedFlowRate) - oldFlowRate; + FlowRate currentAdjustmentFlowRate = _getPoolAdjustmentFlowRate(eff, address(to)); + + Time t = Time.wrap(uint32(block.timestamp)); + (fromUIndexData, pdpIndex, newDistributionFlowRate) = + fromUIndexData.shift_flow2b(pdpIndex, flowRateDelta + currentAdjustmentFlowRate, t); + newActualFlowRate = + oldFlowRate + (newDistributionFlowRate - oldDistributionFlowRate) - currentAdjustmentFlowRate; + + actualFlowRate = int256(FlowRate.unwrap(newActualFlowRate)).toInt96(); + totalDistributionFlowRate = int256(FlowRate.unwrap(newDistributionFlowRate)).toInt96(); + + if (actualFlowRate < 0) { + actualFlowRate = 0; + } + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function estimateDistributionActualAmount( + ISuperfluidToken token, + address from, + ISuperfluidPool to, + uint256 requestedAmount + ) external view override returns (uint256 actualAmount) { + bytes memory eff = abi.encode(token); + + Value actualDistributionAmount; + (,, actualDistributionAmount) = + _getUIndex(eff, from).shift2b(_getPDPIndex("", address(to)), Value.wrap(requestedAmount.toInt256())); + + actualAmount = uint256(Value.unwrap(actualDistributionAmount)); + } + + function _createPool(ISuperfluidToken token, address admin, PoolConfig memory config) + internal + returns (ISuperfluidPool pool) + { + // @note ensure if token and admin are the same that nothing funky happens with echidna + if (admin == address(0)) revert GDA_NO_ZERO_ADDRESS_ADMIN(); + if (_isPool(token, admin)) revert GDA_ADMIN_CANNOT_BE_POOL(); + + pool = ISuperfluidPool( + address(SuperfluidPoolDeployerLibrary.deploy(address(superfluidPoolBeacon), admin, token, config)) + ); + + // @note We utilize the storage slot for Universal Index State + // to store whether an account is a pool or not + bytes32[] memory data = new bytes32[](1); + data[0] = bytes32(uint256(1)); + token.updateAgreementStateSlot(address(pool), _UNIVERSAL_INDEX_STATE_SLOT_ID, data); + + IPoolAdminNFT poolAdminNFT = IPoolAdminNFT(_getPoolAdminNFTAddress(token)); + + if (address(poolAdminNFT) != address(0)) { + uint256 gasLeftBefore = gasleft(); + // solhint-disable-next-line no-empty-blocks + try poolAdminNFT.mint(address(pool)) { } + catch { + SafeGasLibrary._revertWhenOutOfGas(gasLeftBefore); + } + } + + emit PoolCreated(token, admin, pool); + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function createPool(ISuperfluidToken token, address admin, PoolConfig memory config) + external + override + returns (ISuperfluidPool pool) + { + return _createPool(token, admin, config); + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function updateMemberUnits(ISuperfluidPool pool, address memberAddress, uint128 newUnits, bytes calldata ctx) + external + override + returns (bytes memory newCtx) + { + // Only the admin can update member units here + if (AgreementLibrary.authorizeTokenAccess(pool.superToken(), ctx).msgSender != pool.admin()) { + revert GDA_NOT_POOL_ADMIN(); + } + newCtx = ctx; + + pool.updateMemberUnits(memberAddress, newUnits); + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function claimAll(ISuperfluidPool pool, address memberAddress, bytes calldata ctx) + external + override + returns (bytes memory newCtx) + { + AgreementLibrary.authorizeTokenAccess(pool.superToken(), ctx); + newCtx = ctx; + + pool.claimAll(memberAddress); + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function connectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) { + return connectPool(pool, true, ctx); + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function disconnectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) { + return connectPool(pool, false, ctx); + } + + // @note setPoolConnection function naming + function connectPool(ISuperfluidPool pool, bool doConnect, bytes calldata ctx) + public + returns (bytes memory newCtx) + { + ISuperfluidToken token = pool.superToken(); + ISuperfluid.Context memory currentContext = AgreementLibrary.authorizeTokenAccess(token, ctx); + address msgSender = currentContext.msgSender; + newCtx = ctx; + bool isConnected = _isMemberConnected(token, address(pool), msgSender); + if (doConnect != isConnected) { + assert( + SuperfluidPool(address(pool)).operatorConnectMember( + msgSender, doConnect, uint32(currentContext.timestamp) + ) + ); + + if (doConnect) { + uint32 poolSlotID = + _findAndFillPoolConnectionsBitmap(token, msgSender, bytes32(uint256(uint160(address(pool))))); + + // malicious token can reenter here + // external call to untrusted contract + // what sort of boundary can we trust + token.createAgreement( + _getPoolMemberHash(msgSender, pool), + _encodePoolMemberData(PoolMemberData({ poolID: poolSlotID, pool: address(pool) })) + ); + } else { + (, PoolMemberData memory poolMemberData) = _getPoolMemberData(token, msgSender, pool); + token.terminateAgreement(_getPoolMemberHash(msgSender, pool), 1); + + _clearPoolConnectionsBitmap(token, msgSender, poolMemberData.poolID); + } + + emit PoolConnectionUpdated(token, pool, msgSender, doConnect, currentContext.userData); + } + } + + function _isMemberConnected(ISuperfluidToken token, address pool, address member) internal view returns (bool) { + (bool exist,) = _getPoolMemberData(token, member, ISuperfluidPool(pool)); + return exist; + } + + function isMemberConnected(ISuperfluidPool pool, address member) external view override returns (bool) { + return _isMemberConnected(pool.superToken(), address(pool), member); + } + + function appendIndexUpdateByPool(ISuperfluidToken token, BasicParticle memory p, Time t) external returns (bool) { + if (_isPool(token, msg.sender) == false) { + revert GDA_ONLY_SUPER_TOKEN_POOL(); + } + bytes memory eff = abi.encode(token); + _setUIndex(eff, msg.sender, _getUIndex(eff, msg.sender).mappend(p)); + _setPoolAdjustmentFlowRate(eff, msg.sender, true, /* doShift? */ p.flow_rate(), t); + return true; + } + + function poolSettleClaim(ISuperfluidToken superToken, address claimRecipient, int256 amount) + external + returns (bool) + { + if (_isPool(superToken, msg.sender) == false) { + revert GDA_ONLY_SUPER_TOKEN_POOL(); + } + + // _poolSettleClaim() + _doShift(abi.encode(superToken), msg.sender, claimRecipient, Value.wrap(amount)); + return true; + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function distribute( + ISuperfluidToken token, + address from, + ISuperfluidPool pool, + uint256 requestedAmount, + bytes calldata ctx + ) external override returns (bytes memory newCtx) { + ISuperfluid.Context memory currentContext = AgreementLibrary.authorizeTokenAccess(token, ctx); + + newCtx = ctx; + + if (_isPool(token, address(pool)) == false) { + revert GDA_ONLY_SUPER_TOKEN_POOL(); + } + + // you cannot distribute if admin is not equal to the ctx.msgSender + if (!pool.distributionFromAnyAddress()) { + if (pool.admin() != currentContext.msgSender) { + revert GDA_DISTRIBUTE_FROM_ANY_ADDRESS_NOT_ALLOWED(); + } + } + + // the from address must be the same as the ctx.msgSender + // there is no ACL support + if (from != currentContext.msgSender) { + revert GDA_DISTRIBUTE_FOR_OTHERS_NOT_ALLOWED(); + } + + (, Value actualAmount) = _doDistributeViaPool( + abi.encode(token), currentContext.msgSender, address(pool), Value.wrap(requestedAmount.toInt256()) + ); + + if (token.isAccountCriticalNow(from)) { + revert GDA_INSUFFICIENT_BALANCE(); + } + + // TODO: tokens are moving from sender => pool, including a transfer event makes sense here + // trigger from the supertoken contract - @note this is possible since solc 0.8.21 + + emit InstantDistributionUpdated( + token, + pool, + from, + currentContext.msgSender, + requestedAmount, + uint256(Value.unwrap(actualAmount)), // upcast from int256 -> uint256 is safe + currentContext.userData + ); + } + + // solhint-disable-next-line contract-name-camelcase + struct _StackVars_DistributeFlow { + ISuperfluid.Context currentContext; + bytes32 distributionFlowHash; + FlowRate oldFlowRate; + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function distributeFlow( + ISuperfluidToken token, + address from, + ISuperfluidPool pool, + int96 requestedFlowRate, + bytes calldata ctx + ) external override returns (bytes memory newCtx) { + if (_isPool(token, address(pool)) == false) { + revert GDA_ONLY_SUPER_TOKEN_POOL(); + } + if (requestedFlowRate < 0) { + revert GDA_NO_NEGATIVE_FLOW_RATE(); + } + + _StackVars_DistributeFlow memory flowVars; + { + flowVars.currentContext = AgreementLibrary.authorizeTokenAccess(token, ctx); + flowVars.distributionFlowHash = _getFlowDistributionHash(from, pool); + flowVars.oldFlowRate = _getFlowRate(abi.encode(token), flowVars.distributionFlowHash); + } + + newCtx = ctx; + + // we must check if the requestedFlowRate is greater than 0 here + // otherwise we will block liquidators from closing streams in pools + // where the pool config has distributionFromAnyAddress set to false + if (requestedFlowRate > 0 && !pool.distributionFromAnyAddress()) { + if (pool.admin() != flowVars.currentContext.msgSender) { + revert GDA_DISTRIBUTE_FROM_ANY_ADDRESS_NOT_ALLOWED(); + } + } + + (, FlowRate actualFlowRate, FlowRate newDistributionFlowRate) = _doDistributeFlowViaPool( + abi.encode(token), + from, + address(pool), + flowVars.distributionFlowHash, + FlowRate.wrap(requestedFlowRate), + Time.wrap(uint32(flowVars.currentContext.timestamp)) + ); + + // handle distribute flow on behalf of someone else + // @note move to internal maybe + { + if (from != flowVars.currentContext.msgSender) { + if (requestedFlowRate > 0) { + // @note no ACL support for now + // revert if trying to distribute on behalf of others + revert GDA_DISTRIBUTE_FOR_OTHERS_NOT_ALLOWED(); + } else { + // liquidation case, requestedFlowRate == 0 + (int256 availableBalance,,) = token.realtimeBalanceOf(from, flowVars.currentContext.timestamp); + // StackVarsLiquidation used to handle good ol' stack too deep + StackVarsLiquidation memory liquidationData; + { + liquidationData.token = token; + liquidationData.sender = from; + liquidationData.liquidator = flowVars.currentContext.msgSender; + liquidationData.distributionFlowHash = flowVars.distributionFlowHash; + liquidationData.signedTotalGDADeposit = + _getUIndexData(abi.encode(token), from).totalBuffer.toInt256(); + liquidationData.availableBalance = availableBalance; + } + // closing stream on behalf of someone else: liquidation case + if (availableBalance < 0) { + _makeLiquidationPayouts(liquidationData); + } else { + revert GDA_NON_CRITICAL_SENDER(); + } + } + } + } + + { + _adjustBuffer(token, address(pool), from, flowVars.distributionFlowHash, actualFlowRate); + } + + // ensure sender has enough balance to execute transaction + if (from == flowVars.currentContext.msgSender) { + (int256 availableBalance,,) = token.realtimeBalanceOf(from, flowVars.currentContext.timestamp); + // if from == msg.sender + if (requestedFlowRate > 0 && availableBalance < 0) { + revert GDA_INSUFFICIENT_BALANCE(); + } + } + + // handleFlowNFT() - mint/burn FlowNFT to flow distributor + { + address constantOutflowNFTAddress = _getConstantOutflowNFTAddress(token); + + if (constantOutflowNFTAddress != address(0)) { + uint256 gasLeftBefore; + // create flow (mint) + if (requestedFlowRate > 0 && FlowRate.unwrap(flowVars.oldFlowRate) == 0) { + gasLeftBefore = gasleft(); + // solhint-disable-next-line no-empty-blocks + try IConstantOutflowNFT(constantOutflowNFTAddress).onCreate(token, from, address(pool)) { } + catch { + SafeGasLibrary._revertWhenOutOfGas(gasLeftBefore); + } + } + + // update flow (update metadata) + if (requestedFlowRate > 0 && FlowRate.unwrap(flowVars.oldFlowRate) > 0) { + gasLeftBefore = gasleft(); + // solhint-disable-next-line no-empty-blocks + try IConstantOutflowNFT(constantOutflowNFTAddress).onUpdate(token, from, address(pool)) { } + catch { + SafeGasLibrary._revertWhenOutOfGas(gasLeftBefore); + } + } + + // delete flow (burn) + if (requestedFlowRate == 0) { + gasLeftBefore = gasleft(); + // solhint-disable-next-line no-empty-blocks + try IConstantOutflowNFT(constantOutflowNFTAddress).onDelete(token, from, address(pool)) { } + catch { + SafeGasLibrary._revertWhenOutOfGas(gasLeftBefore); + } + } + } + } + + { + (address adjustmentFlowRecipient,, int96 adjustmentFlowRate) = + _getPoolAdjustmentFlowInfo(abi.encode(token), address(pool)); + + emit FlowDistributionUpdated( + token, + pool, + from, + flowVars.currentContext.msgSender, + int256(FlowRate.unwrap(flowVars.oldFlowRate)).toInt96(), + int256(FlowRate.unwrap(actualFlowRate)).toInt96(), + int256(FlowRate.unwrap(newDistributionFlowRate)).toInt96(), + adjustmentFlowRecipient, + adjustmentFlowRate, + flowVars.currentContext.userData + ); + } + } + + /** + * @notice Checks whether or not the NFT hook can be called. + * @dev A staticcall, so `CONSTANT_OUTFLOW_NFT` must be a view otherwise the assumption is that it reverts + * @param token the super token that is being streamed + * @return constantOutflowNFTAddress the address returned by low level call + */ + function _getConstantOutflowNFTAddress(ISuperfluidToken token) + internal + view + returns (address constantOutflowNFTAddress) + { + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory data) = + address(token).staticcall(abi.encodeWithSelector(ISuperToken.CONSTANT_OUTFLOW_NFT.selector)); + + if (success) { + // @note We are aware this may revert if a Custom SuperToken's + // CONSTANT_OUTFLOW_NFT does not return data that can be + // decoded to an address. This would mean it was intentionally + // done by the creator of the Custom SuperToken logic and is + // fully expected to revert in that case as the author desired. + constantOutflowNFTAddress = abi.decode(data, (address)); + } + } + + function _getPoolAdminNFTAddress(ISuperfluidToken token) internal view returns (address poolAdminNFTAddress) { + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory data) = + address(token).staticcall(abi.encodeWithSelector(ISuperToken.POOL_ADMIN_NFT.selector)); + + if (success) { + // @note We are aware this may revert if a Custom SuperToken's + // POOL_ADMIN_NFT does not return data that can be + // decoded to an address. This would mean it was intentionally + // done by the creator of the Custom SuperToken logic and is + // fully expected to revert in that case as the author desired. + poolAdminNFTAddress = abi.decode(data, (address)); + } + } + + function _makeLiquidationPayouts(StackVarsLiquidation memory data) internal { + (, FlowDistributionData memory flowDistributionData) = + _getFlowDistributionData(ISuperfluidToken(data.token), data.distributionFlowHash); + int256 signedSingleDeposit = flowDistributionData.buffer.toInt256(); + + bool isCurrentlyPatricianPeriod; + + { + (uint256 liquidationPeriod, uint256 patricianPeriod) = + SolvencyHelperLibrary.decode3PsData(ISuperfluid(_host), data.token); + isCurrentlyPatricianPeriod = SolvencyHelperLibrary.isPatricianPeriod( + data.availableBalance, data.signedTotalGDADeposit, liquidationPeriod, patricianPeriod + ); + } + + int256 totalRewardLeft = data.availableBalance + data.signedTotalGDADeposit; + + // critical case + if (totalRewardLeft >= 0) { + int256 rewardAmount = (signedSingleDeposit * totalRewardLeft) / data.signedTotalGDADeposit; + data.token.makeLiquidationPayoutsV2( + data.distributionFlowHash, + abi.encode(2, isCurrentlyPatricianPeriod ? 0 : 1), + data.liquidator, + isCurrentlyPatricianPeriod, + data.sender, + rewardAmount.toUint256(), + rewardAmount * -1 + ); + } else { + int256 rewardAmount = signedSingleDeposit; + // bailout case + data.token.makeLiquidationPayoutsV2( + data.distributionFlowHash, + abi.encode(2, 2), + data.liquidator, + false, + data.sender, + rewardAmount.toUint256(), + totalRewardLeft * -1 + ); + } + } + + function _adjustBuffer(ISuperfluidToken token, address pool, address from, bytes32 flowHash, FlowRate newFlowRate) + internal + { + // not using oldFlowRate in this model + // surprising effect: reducing flow rate may require more buffer when liquidation_period adjusted upward + ISuperfluidGovernance gov = ISuperfluidGovernance(ISuperfluid(_host).getGovernance()); + uint256 minimumDeposit = + gov.getConfigAsUint256(ISuperfluid(msg.sender), ISuperfluidToken(token), SUPERTOKEN_MINIMUM_DEPOSIT_KEY); + + (uint256 liquidationPeriod,) = SolvencyHelperLibrary.decode3PsData(ISuperfluid(_host), ISuperfluidToken(token)); + + (, FlowDistributionData memory flowDistributionData) = + _getFlowDistributionData(ISuperfluidToken(token), flowHash); + + // @note downcasting from uint256 -> uint32 for liquidation period + Value newBufferAmount = newFlowRate.mul(Time.wrap(uint32(liquidationPeriod))); + + if (Value.unwrap(newBufferAmount).toUint256() < minimumDeposit && FlowRate.unwrap(newFlowRate) > 0) { + newBufferAmount = Value.wrap(minimumDeposit.toInt256()); + } + + Value bufferDelta = newBufferAmount - Value.wrap(uint256(flowDistributionData.buffer).toInt256()); + + { + bytes32[] memory data = _encodeFlowDistributionData( + FlowDistributionData({ + lastUpdated: uint32(block.timestamp), + flowRate: int256(FlowRate.unwrap(newFlowRate)).toInt96(), + buffer: uint256(Value.unwrap(newBufferAmount)) // upcast to uint256 is safe + }) + ); + + ISuperfluidToken(token).updateAgreementData(flowHash, data); + } + + UniversalIndexData memory universalIndexData = _getUIndexData(abi.encode(token), from); + universalIndexData.totalBuffer = + // new buffer + (universalIndexData.totalBuffer.toInt256() + Value.unwrap(bufferDelta)).toUint256(); + ISuperfluidToken(token).updateAgreementStateSlot( + from, _UNIVERSAL_INDEX_STATE_SLOT_ID, _encodeUniversalIndexData(universalIndexData) + ); + + { + emit BufferAdjusted( + ISuperfluidToken(token), + ISuperfluidPool(pool), + from, + Value.unwrap(bufferDelta), + Value.unwrap(newBufferAmount).toUint256(), + universalIndexData.totalBuffer + ); + } + } + + // Solvency Related Getters + function isPatricianPeriodNow(ISuperfluidToken token, address account) + external + view + override + returns (bool isCurrentlyPatricianPeriod, uint256 timestamp) + { + timestamp = ISuperfluid(_host).getNow(); + isCurrentlyPatricianPeriod = isPatricianPeriod(token, account, timestamp); + } + + function isPatricianPeriod(ISuperfluidToken token, address account, uint256 timestamp) + public + view + override + returns (bool) + { + (int256 availableBalance,,) = token.realtimeBalanceOf(account, timestamp); + if (availableBalance >= 0) { + return true; + } + + (uint256 liquidationPeriod, uint256 patricianPeriod) = + SolvencyHelperLibrary.decode3PsData(ISuperfluid(_host), token); + + return SolvencyHelperLibrary.isPatricianPeriod( + availableBalance, + _getUIndexData(abi.encode(token), account).totalBuffer.toInt256(), + liquidationPeriod, + patricianPeriod + ); + } + + // Hash Getters + + function _getPoolMemberHash(address poolMember, ISuperfluidPool pool) internal view returns (bytes32) { + return keccak256(abi.encode(block.chainid, "poolMember", poolMember, address(pool))); + } + + function _getFlowDistributionHash(address from, ISuperfluidPool to) internal view returns (bytes32) { + return keccak256(abi.encode(block.chainid, "distributionFlow", from, to)); + } + + function _getPoolAdjustmentFlowHash(address from, address to) internal view returns (bytes32) { + // this will never be in conflict with other flow has types + return keccak256(abi.encode(block.chainid, "poolAdjustmentFlow", from, to)); + } + + // # Universal Index operations + // + // Universal Index packing: + // store buffer (96) and one bit to specify is pool in free + // -------- ------------------ ------------------ ------------------ ------------------ + // WORD 1: | flowRate | settledAt | totalBuffer | isPool | + // -------- ------------------ ------------------ ------------------ ------------------ + // | 96b | 32b | 96b | 32b | + // -------- ------------------ ------------------ ------------------ ------------------ + // WORD 2: | settledValue | + // -------- ------------------ ------------------ ------------------ ------------------ + // | 256b | + // -------- ------------------ ------------------ ------------------ ------------------ + + function _encodeUniversalIndexData(BasicParticle memory p, uint256 buffer, bool isPool_) + internal + pure + returns (bytes32[] memory data) + { + data = new bytes32[](2); + data[0] = bytes32( + (uint256(int256(FlowRate.unwrap(p.flow_rate()))) << 160) | (uint256(Time.unwrap(p.settled_at())) << 128) + | (uint256(buffer.toUint96()) << 32) | (isPool_ ? 1 : 0) + ); + data[1] = bytes32(uint256(Value.unwrap(p._settled_value))); + } + + function _encodeUniversalIndexData(UniversalIndexData memory uIndexData) + internal + pure + returns (bytes32[] memory data) + { + data = new bytes32[](2); + data[0] = bytes32( + (uint256(int256(uIndexData.flowRate)) << 160) | (uint256(uIndexData.settledAt) << 128) + | (uint256(uIndexData.totalBuffer.toUint96()) << 32) | (uIndexData.isPool ? 1 : 0) + ); + data[1] = bytes32(uint256(uIndexData.settledValue)); + } + + function _decodeUniversalIndexData(bytes32[] memory data) + internal + pure + returns (bool exists, UniversalIndexData memory universalIndexData) + { + uint256 a = uint256(data[0]); + uint256 b = uint256(data[1]); + + exists = a > 0 || b > 0; + + if (exists) { + universalIndexData.flowRate = int96(int256(a >> 160) & int256(uint256(type(uint96).max))); + universalIndexData.settledAt = uint32(uint256(a >> 128) & uint256(type(uint32).max)); + universalIndexData.totalBuffer = uint256(a >> 32) & uint256(type(uint96).max); + universalIndexData.isPool = ((a << 224) >> 224) & 1 == 1; + universalIndexData.settledValue = int256(b); + } + } + + function _getUIndexData(bytes memory eff, address owner) + internal + view + returns (UniversalIndexData memory universalIndexData) + { + (, universalIndexData) = _decodeUniversalIndexData( + ISuperfluidToken(abi.decode(eff, (address))).getAgreementStateSlot( + address(this), owner, _UNIVERSAL_INDEX_STATE_SLOT_ID, 2 + ) + ); + } + + function _getBasicParticleFromUIndex(UniversalIndexData memory universalIndexData) + internal + pure + returns (BasicParticle memory particle) + { + particle._flow_rate = FlowRate.wrap(universalIndexData.flowRate); + particle._settled_at = Time.wrap(universalIndexData.settledAt); + particle._settled_value = Value.wrap(universalIndexData.settledValue); + } + + // TokenMonad virtual functions + function _getUIndex(bytes memory eff, address owner) internal view override returns (BasicParticle memory uIndex) { + (, UniversalIndexData memory universalIndexData) = _decodeUniversalIndexData( + ISuperfluidToken(abi.decode(eff, (address))).getAgreementStateSlot( + address(this), owner, _UNIVERSAL_INDEX_STATE_SLOT_ID, 2 + ) + ); + uIndex = _getBasicParticleFromUIndex(universalIndexData); + } + + function _setUIndex(bytes memory eff, address owner, BasicParticle memory p) + internal + override + returns (bytes memory) + { + UniversalIndexData memory universalIndexData = _getUIndexData(eff, owner); + + ISuperfluidToken(abi.decode(eff, (address))).updateAgreementStateSlot( + owner, + _UNIVERSAL_INDEX_STATE_SLOT_ID, + _encodeUniversalIndexData(p, universalIndexData.totalBuffer, universalIndexData.isPool) + ); + + return eff; + } + + function _getPDPIndex( + bytes memory, // eff, + address pool + ) internal view override returns (PDPoolIndex memory) { + ISuperfluidPool.PoolIndexData memory data = SuperfluidPool(pool).getIndex(); + return SuperfluidPool(pool).poolIndexDataToPDPoolIndex(data); + } + + function _setPDPIndex(bytes memory eff, address pool, PDPoolIndex memory p) + internal + override + returns (bytes memory) + { + assert(SuperfluidPool(pool).operatorSetIndex(p)); + + return eff; + } + + function _getFlowRate(bytes memory eff, bytes32 distributionFlowHash) internal view override returns (FlowRate) { + (, FlowDistributionData memory data) = + _getFlowDistributionData(ISuperfluidToken(abi.decode(eff, (address))), distributionFlowHash); + return FlowRate.wrap(data.flowRate); + } + + function _setFlowInfo( + bytes memory eff, + bytes32 flowHash, + address, // from, + address, // to, + FlowRate newFlowRate, + FlowRate // flowRateDelta + ) internal override returns (bytes memory) { + address token = abi.decode(eff, (address)); + (, FlowDistributionData memory flowDistributionData) = + _getFlowDistributionData(ISuperfluidToken(token), flowHash); + + ISuperfluidToken(token).updateAgreementData( + flowHash, + _encodeFlowDistributionData( + FlowDistributionData({ + lastUpdated: uint32(block.timestamp), + flowRate: int256(FlowRate.unwrap(newFlowRate)).toInt96(), + buffer: flowDistributionData.buffer + }) + ) + ); + + return eff; + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function getPoolAdjustmentFlowInfo(ISuperfluidPool pool) + external + view + override + returns (address recipient, bytes32 flowHash, int96 flowRate) + { + return _getPoolAdjustmentFlowInfo(abi.encode(pool.superToken()), address(pool)); + } + + function _getPoolAdjustmentFlowInfo(bytes memory eff, address pool) + internal + view + returns (address adjustmentRecipient, bytes32 flowHash, int96 flowRate) + { + // pool admin is always the adjustment recipient + adjustmentRecipient = ISuperfluidPool(pool).admin(); + flowHash = _getPoolAdjustmentFlowHash(pool, adjustmentRecipient); + return (adjustmentRecipient, flowHash, int256(FlowRate.unwrap(_getFlowRate(eff, flowHash))).toInt96()); + } + + function _getPoolAdjustmentFlowRate(bytes memory eff, address pool) + internal + view + override + returns (FlowRate flowRate) + { + (,, int96 rawFlowRate) = _getPoolAdjustmentFlowInfo(eff, pool); + flowRate = FlowRate.wrap(int128(rawFlowRate)); // upcasting to int128 is safe + } + + function getPoolAdjustmentFlowRate(address pool) external view override returns (int96) { + ISuperfluidToken token = ISuperfluidPool(pool).superToken(); + return int256(FlowRate.unwrap(_getPoolAdjustmentFlowRate(abi.encode(token), pool))).toInt96(); + } + + function _setPoolAdjustmentFlowRate(bytes memory eff, address pool, FlowRate flowRate, Time t) + internal + override + returns (bytes memory) + { + return _setPoolAdjustmentFlowRate(eff, pool, false, /* doShift? */ flowRate, t); + } + + function _setPoolAdjustmentFlowRate(bytes memory eff, address pool, bool doShiftFlow, FlowRate flowRate, Time t) + internal + returns (bytes memory) + { + // @note should this also always be + address adjustmentRecipient = ISuperfluidPool(pool).admin(); + bytes32 adjustmentFlowHash = _getPoolAdjustmentFlowHash(pool, adjustmentRecipient); + + if (doShiftFlow) { + flowRate = flowRate + _getFlowRate(eff, adjustmentFlowHash); + } + eff = _doFlow(eff, pool, adjustmentRecipient, adjustmentFlowHash, flowRate, t); + return eff; + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function isPool(ISuperfluidToken token, address account) external view override returns (bool) { + return _isPool(token, account); + } + + function _isPool(ISuperfluidToken token, address account) internal view returns (bool exists) { + // @note see createPool, we retrieve the isPool bit from + // UniversalIndex for this pool to determine whether the account + // is a pool + exists = ( + (uint256(token.getAgreementStateSlot(address(this), account, _UNIVERSAL_INDEX_STATE_SLOT_ID, 1)[0]) << 224) + >> 224 + ) & 1 == 1; + } + + // FlowDistributionData data packing: + // -------- ---------- ------------- ---------- -------- + // WORD A: | reserved | lastUpdated | flowRate | buffer | + // -------- ---------- ------------- ---------- -------- + // | 32 | 32 | 96 | 96 | + // -------- ---------- ------------- ---------- -------- + + function _encodeFlowDistributionData(FlowDistributionData memory flowDistributionData) + internal + pure + returns (bytes32[] memory data) + { + data = new bytes32[](1); + data[0] = bytes32( + (uint256(uint32(flowDistributionData.lastUpdated)) << 192) + | (uint256(uint96(flowDistributionData.flowRate)) << 96) | uint256(flowDistributionData.buffer) + ); + } + + function _decodeFlowDistributionData(uint256 data) + internal + pure + returns (bool exist, FlowDistributionData memory flowDistributionData) + { + exist = data > 0; + if (exist) { + flowDistributionData.lastUpdated = uint32((data >> 192) & uint256(type(uint32).max)); + flowDistributionData.flowRate = int96(int256(data >> 96)); + flowDistributionData.buffer = uint96(data & uint256(type(uint96).max)); + } + } + + function _getFlowDistributionData(ISuperfluidToken token, bytes32 distributionFlowHash) + internal + view + returns (bool exist, FlowDistributionData memory flowDistributionData) + { + (exist, flowDistributionData) = + _decodeFlowDistributionData(uint256(token.getAgreementData(address(this), distributionFlowHash, 1)[0])); + } + + // PoolMemberData data packing: + // -------- ---------- -------- ------------- + // WORD A: | reserved | poolID | poolAddress | + // -------- ---------- -------- ------------- + // | 64 | 32 | 160 | + // -------- ---------- -------- ------------- + + function _encodePoolMemberData(PoolMemberData memory poolMemberData) + internal + pure + returns (bytes32[] memory data) + { + data = new bytes32[](1); + data[0] = bytes32((uint256(uint32(poolMemberData.poolID)) << 160) | uint256(uint160(poolMemberData.pool))); + } + + function _decodePoolMemberData(uint256 data) + internal + pure + returns (bool exist, PoolMemberData memory poolMemberData) + { + exist = data > 0; + if (exist) { + poolMemberData.pool = address(uint160(data & uint256(type(uint160).max))); + poolMemberData.poolID = uint32(data >> 160); + } + } + + function _getPoolMemberData(ISuperfluidToken token, address poolMember, ISuperfluidPool pool) + internal + view + returns (bool exist, PoolMemberData memory poolMemberData) + { + (exist, poolMemberData) = _decodePoolMemberData( + uint256(token.getAgreementData(address(this), _getPoolMemberHash(poolMember, pool), 1)[0]) + ); + } + + // SlotsBitmap Pool Data: + function _findAndFillPoolConnectionsBitmap(ISuperfluidToken token, address poolMember, bytes32 poolID) + private + returns (uint32 slotId) + { + return SlotsBitmapLibrary.findEmptySlotAndFill( + token, poolMember, _POOL_SUBS_BITMAP_STATE_SLOT_ID, _POOL_CONNECTIONS_DATA_STATE_SLOT_ID_START, poolID + ); + } + + function _clearPoolConnectionsBitmap(ISuperfluidToken token, address poolMember, uint32 slotId) private { + SlotsBitmapLibrary.clearSlot(token, poolMember, _POOL_SUBS_BITMAP_STATE_SLOT_ID, slotId); + } + + function _listPoolConnectionIds(ISuperfluidToken token, address subscriber) + private + view + returns (uint32[] memory slotIds, bytes32[] memory pidList) + { + (slotIds, pidList) = SlotsBitmapLibrary.listData( + token, subscriber, _POOL_SUBS_BITMAP_STATE_SLOT_ID, _POOL_CONNECTIONS_DATA_STATE_SLOT_ID_START + ); + } +} diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/PoolAdminNFT.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/PoolAdminNFT.sol new file mode 100644 index 0000000000..937fd09ab6 --- /dev/null +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/PoolAdminNFT.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { IPoolAdminNFT } from "../../interfaces/agreements/gdav1/IPoolAdminNFT.sol"; +import { PoolNFTBase } from "./PoolNFTBase.sol"; +import { ISuperfluid } from "../../interfaces/superfluid/ISuperfluid.sol"; +import { ISuperfluidPool } from "../../interfaces/agreements/gdav1/ISuperfluidPool.sol"; +import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol"; + +contract PoolAdminNFT is PoolNFTBase, IPoolAdminNFT { + //// Storage Variables //// + + /// NOTE: The storage variables in this contract MUST NOT: + /// - change the ordering of the existing variables + /// - change any of the variable types + /// - rename any of the existing variables + /// - remove any of the existing variables + + /// @notice A mapping from token id to PoolAdminNFT data + /// PoolAdminNFTData: { address pool, address admin } + /// @dev The token id is uint256(keccak256(abi.encode(pool, admin))) + mapping(uint256 => PoolAdminNFTData) internal _poolAdminDataByTokenId; + + constructor(ISuperfluid host) PoolNFTBase(host) { } + + // note that this is used so we don't upgrade to wrong logic contract + function proxiableUUID() public pure override returns (bytes32) { + return keccak256("org.superfluid-finance.contracts.PoolAdminNFT.implementation"); + } + + function _ownerOf(uint256 tokenId) internal view override returns (address) { + return _poolAdminDataByTokenId[tokenId].admin; + } + + function poolAdminDataByTokenId(uint256 tokenId) external view override returns (PoolAdminNFTData memory data) { + return _poolAdminDataByTokenId[tokenId]; + } + + /// @notice Reverts - Transfer of pool member NFT is not allowed. + /// @dev We revert when users attempt to transfer pool member NFTs. + function _transfer( + address, // from, + address, // to, + uint256 // tokenId + ) internal pure override { + revert POOL_NFT_TRANSFER_NOT_ALLOWED(); + } + + function getTokenId(address pool, address admin) external view override returns (uint256 tokenId) { + return _getTokenId(pool, admin); + } + + function _getTokenId(address pool, address admin) internal view returns (uint256 tokenId) { + return uint256(keccak256(abi.encode("PoolAdminNFT", block.chainid, pool, admin))); + } + + /// @inheritdoc PoolNFTBase + function tokenURI(uint256 tokenId) external view override(IERC721Metadata, PoolNFTBase) returns (string memory) { + return super._tokenURI(tokenId); + } + + function mint(address pool) external { + _mint(pool); + } + + /// @notice Mints `newTokenId` and transfers it to `admin` + /// @dev `pool` must be a registered pool in the GDA. + /// `newTokenId` must not exist, `admin` cannot be `address(0)` and we emit a {Transfer} event. + /// `admin` cannot be equal to `pool`. + /// @param pool The pool address + function _mint(address pool) internal { + ISuperfluidToken superToken = ISuperfluidPool(pool).superToken(); + if (!GENERAL_DISTRIBUTION_AGREEMENT_V1.isPool(superToken, pool)) { + revert POOL_NFT_NOT_REGISTERED_POOL(); + } + ISuperfluidPool poolContract = ISuperfluidPool(pool); + address admin = poolContract.admin(); + assert(pool != admin); + + uint256 newTokenId = _getTokenId(pool, admin); + assert(!_exists(newTokenId)); + + // update mapping for new NFT to be minted + _poolAdminDataByTokenId[newTokenId] = PoolAdminNFTData(pool, admin); + + // emit mint of new pool admin token with newTokenId + emit Transfer(address(0), admin, newTokenId); + } +} diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol new file mode 100644 index 0000000000..8999f09a98 --- /dev/null +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { IPoolMemberNFT } from "../../interfaces/agreements/gdav1/IPoolMemberNFT.sol"; +import { PoolNFTBase } from "./PoolNFTBase.sol"; +import { ISuperfluid } from "../../interfaces/superfluid/ISuperfluid.sol"; +import { ISuperfluidPool } from "../../interfaces/agreements/gdav1/ISuperfluidPool.sol"; +import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol"; + +contract PoolMemberNFT is PoolNFTBase, IPoolMemberNFT { + //// Storage Variables //// + + /// NOTE: The storage variables in this contract MUST NOT: + /// - change the ordering of the existing variables + /// - change any of the variable types + /// - rename any of the existing variables + /// - remove any of the existing variables + + /// @notice A mapping from token id to PoolMemberNFT data + /// PoolMemberNFTData: { address pool, address member, uint128 units } + /// @dev The token id is uint256(keccak256(abi.encode(pool, member))) + mapping(uint256 => PoolMemberNFTData) internal _poolMemberDataByTokenId; + + constructor(ISuperfluid host) PoolNFTBase(host) { } + + // note that this is used so we don't upgrade to wrong logic contract + function proxiableUUID() public pure override returns (bytes32) { + return keccak256("org.superfluid-finance.contracts.PoolMemberNFT.implementation"); + } + + function _ownerOf(uint256 tokenId) internal view override returns (address) { + return _poolMemberDataByTokenId[tokenId].member; + } + + function poolMemberDataByTokenId(uint256 tokenId) public view override returns (PoolMemberNFTData memory data) { + return _poolMemberDataByTokenId[tokenId]; + } + + /// @notice Reverts - Transfer of pool member NFT is not allowed. + /// @dev We revert when users attempt to transfer pool member NFTs. + function _transfer( + address, // from, + address, // to, + uint256 // tokenId + ) internal pure override { + revert POOL_NFT_TRANSFER_NOT_ALLOWED(); + } + + function getTokenId(address pool, address member) external view override returns (uint256 tokenId) { + return _getTokenId(pool, member); + } + + function _getTokenId(address pool, address member) internal view returns (uint256 tokenId) { + return uint256(keccak256(abi.encode("PoolMemberNFT", block.chainid, pool, member))); + } + + /// @inheritdoc PoolNFTBase + function tokenURI(uint256 tokenId) external view override(IERC721Metadata, PoolNFTBase) returns (string memory) { + return super._tokenURI(tokenId); + } + + /// @notice Mints `newTokenId` and transfers it to `member` + /// @dev `pool` must be a registered pool in the GDA. + /// `newTokenId` must not exist, `member` cannot be `address(0)`, `pool` cannot be `address(0)`, + /// and `pool` cannot be `member`. + /// We emit a {Transfer} event. + /// @param pool The pool address + /// @param member The member address + function onCreate(address pool, address member) external override { + _mint(pool, member); + } + + /// @notice Updates token with `tokenId`. + /// @dev `tokenId` must exist AND we emit a {MetadataUpdate} event + /// @param pool The pool address + /// @param member The member address + function onUpdate(address pool, address member) external override { + uint256 tokenId = _getTokenId(pool, member); + address owner = _ownerOf(tokenId); + assert(owner != address(0)); + PoolMemberNFTData storage data = _poolMemberDataByTokenId[tokenId]; + data.units = ISuperfluidPool(data.pool).getUnits(data.member); + + _triggerMetadataUpdate(tokenId); + } + + /// @notice Destroys token with `tokenId` and clears approvals from previous owner. + /// @dev `tokenId` must exist AND we emit a {Transfer} event + /// @param pool The pool address + /// @param member The member address + function onDelete(address pool, address member) external override { + uint256 tokenId = _getTokenId(pool, member); + _burn(tokenId); + } + + function _mint(address pool, address member) internal { + ISuperfluidToken superToken = ISuperfluidPool(pool).superToken(); + if (!GENERAL_DISTRIBUTION_AGREEMENT_V1.isPool(superToken, pool)) { + revert POOL_NFT_NOT_REGISTERED_POOL(); + } + + assert(pool != address(0)); + assert(member != address(0)); + assert(pool != member); + + uint256 newTokenId = _getTokenId(pool, member); + assert(!_exists(newTokenId)); + + uint128 units = ISuperfluidPool(pool).getUnits(member); + + if (units == 0) { + revert POOL_MEMBER_NFT_NO_UNITS(); + } + + // update mapping for new NFT to be minted + _poolMemberDataByTokenId[newTokenId] = PoolMemberNFTData(pool, member, units); + + // emit mint of new pool member token with newTokenId + emit Transfer(address(0), member, newTokenId); + } + + function _burn(uint256 tokenId) internal override { + PoolMemberNFTData storage data = _poolMemberDataByTokenId[tokenId]; + if (ISuperfluidPool(data.pool).getUnits(data.member) > 0) { + revert POOL_MEMBER_NFT_HAS_UNITS(); + } + + address owner = _ownerOf(tokenId); + assert(owner != address(0)); + super._burn(tokenId); + + // remove previous tokenId flow data mapping + delete _poolMemberDataByTokenId[tokenId]; + + // emit burn of pool member token with tokenId + emit Transfer(owner, address(0), tokenId); + } +} diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/PoolNFTBase.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/PoolNFTBase.sol new file mode 100644 index 0000000000..bda4fe2e90 --- /dev/null +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/PoolNFTBase.sol @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +// Notes: We use reserved slots for upgradable contracts. +// solhint-disable max-states-count + +// Notes: We use these interfaces in natspec documentation below, grep @inheritdoc +// solhint-disable-next-line no-unused-import +import { IERC165, IERC721, IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { UUPSProxiable } from "../../upgradability/UUPSProxiable.sol"; +import { ISuperfluid } from "../../interfaces/superfluid/ISuperfluid.sol"; +import { ISuperTokenFactory } from "../../interfaces/superfluid/ISuperTokenFactory.sol"; +import { IPoolNFTBase } from "../../interfaces/agreements/gdav1/IPoolNFTBase.sol"; +import { IGeneralDistributionAgreementV1 } from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; + +abstract contract PoolNFTBase is UUPSProxiable, IPoolNFTBase { + string public constant DEFAULT_BASE_URI = "https://nft.superfluid.finance/pool/v2/getmeta"; + + function baseURI() public pure returns (string memory) { return DEFAULT_BASE_URI; } + + /// @notice Superfluid host contract address + ISuperfluid public immutable HOST; + + /// @notice Superfluid GDAv1 contract address + IGeneralDistributionAgreementV1 public immutable GENERAL_DISTRIBUTION_AGREEMENT_V1; + + //// Storage Variables //// + + /// NOTE: The storage variables in this contract MUST NOT: + /// - change the ordering of the existing variables + /// - change any of the variable types + /// - rename any of the existing variables + /// - remove any of the existing variables + + string internal _name; + string internal _symbol; + + /// @notice Mapping for token approvals + /// @dev tokenID => approved address mapping + mapping(uint256 => address) internal _tokenApprovals; + + /// @notice Mapping for operator approvals + mapping(address => mapping(address => bool)) internal _operatorApprovals; + + /// @notice This allows us to add new storage variables in the base contract + /// without having to worry about messing up the storage layout that exists in COFNFT or CIFNFT. + /// @dev This empty reserved space is put in place to allow future versions to add new + /// variables without shifting down storage in the inheritance chain. + /// Slots 5-21 are reserved for future use. + /// We use this pattern in SuperToken.sol and favor this over the OpenZeppelin pattern + /// as this prevents silly footgunning. + /// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + uint256 internal _reserve5; + uint256 private _reserve6; + uint256 private _reserve7; + uint256 private _reserve8; + uint256 private _reserve9; + uint256 private _reserve10; + uint256 private _reserve11; + uint256 private _reserve12; + uint256 private _reserve13; + uint256 private _reserve14; + uint256 private _reserve15; + uint256 private _reserve16; + uint256 private _reserve17; + uint256 private _reserve18; + uint256 private _reserve19; + uint256 private _reserve20; + uint256 internal _reserve21; + + constructor(ISuperfluid host) { + HOST = host; + GENERAL_DISTRIBUTION_AGREEMENT_V1 = IGeneralDistributionAgreementV1( + address( + ISuperfluid(host).getAgreementClass( + keccak256("org.superfluid-finance.agreements.GeneralDistributionAgreement.v1") + ) + ) + ); + } + + function initialize(string memory nftName, string memory nftSymbol) + external + override + initializer // OpenZeppelin Initializable + { + _name = nftName; + _symbol = nftSymbol; + } + + function updateCode(address newAddress) external override { + ISuperTokenFactory superTokenFactory = HOST.getSuperTokenFactory(); + if (msg.sender != address(superTokenFactory)) { + revert POOL_NFT_ONLY_SUPER_TOKEN_FACTORY(); + } + + UUPSProxiable._updateCodeAddress(newAddress); + } + + /// @notice Emits the MetadataUpdate event with `tokenId` as the argument. + /// @dev Callable by anyone. + /// @param tokenId the token id to trigger a metaupdate for + function triggerMetadataUpdate(uint256 tokenId) external { + _triggerMetadataUpdate(tokenId); + } + + /// @notice This contract supports IERC165, IERC721 and IERC721Metadata + /// @dev This is part of the Standard Interface Detection EIP: https://eips.ethereum.org/EIPS/eip-165 + /// @param interfaceId the XOR of all function selectors in the interface + /// @return boolean true if the interface is supported + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) external pure virtual override returns (bool) { + return interfaceId == 0x01ffc9a7 // ERC165 Interface ID for ERC165 + || interfaceId == 0x80ac58cd // ERC165 Interface ID for ERC721 + || interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata + } + + /// @inheritdoc IERC721 + function ownerOf(uint256 tokenId) public view virtual override returns (address) { + address owner = _ownerOf(tokenId); + if (owner == address(0)) { + revert POOL_NFT_INVALID_TOKEN_ID(); + } + return owner; + } + + /// @notice Returns a hardcoded balance of 1 + /// @dev We always return 1 to avoid the need for additional mapping + /// @return balance = 1 + function balanceOf( + address // owner + ) external pure returns (uint256 balance) { + balance = 1; + } + + /// @notice Returns the name of the NFT + /// @dev Should follow the naming convention: (Pool Admin|Pool Member) NFT + /// @return name of the NFT + function name() external view virtual override returns (string memory) { + return _name; + } + + /// @notice Returns the symbol of the NFT + /// @dev Should follow the naming convention: PA|PM + /// @return symbol of the NFT + function symbol() external view virtual override returns (string memory) { + return _symbol; + } + + /// @notice This returns the Uniform Resource Identifier (URI), where the metadata for the NFT lives. + /// @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. + /// @return the token URI + function tokenURI(uint256 tokenId) external view virtual returns (string memory); + + function _tokenURI(uint256 /*tokenId*/) internal view virtual returns (string memory) { + return string(abi.encodePacked(baseURI())); + } + + /// @inheritdoc IERC721 + function approve(address to, uint256 tokenId) public virtual override { + address owner = PoolNFTBase.ownerOf(tokenId); + if (to == owner) { + revert POOL_NFT_APPROVE_TO_CURRENT_OWNER(); + } + + if (msg.sender != owner && !isApprovedForAll(owner, msg.sender)) { + revert POOL_NFT_APPROVE_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL(); + } + + _approve(to, tokenId); + } + + /// @inheritdoc IERC721 + function getApproved(uint256 tokenId) public view virtual override returns (address) { + _requireMinted(tokenId); + + return _tokenApprovals[tokenId]; + } + + /// @inheritdoc IERC721 + function setApprovalForAll(address operator, bool approved) external virtual override { + _setApprovalForAll(msg.sender, operator, approved); + } + + /// @inheritdoc IERC721 + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + return _operatorApprovals[owner][operator]; + } + + /// @inheritdoc IERC721 + function transferFrom(address from, address to, uint256 tokenId) external virtual override { + if (!_isApprovedOrOwner(msg.sender, tokenId)) { + revert POOL_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL(); + } + + _transfer(from, to, tokenId); + } + + /// @inheritdoc IERC721 + function safeTransferFrom(address from, address to, uint256 tokenId) external virtual override { + safeTransferFrom(from, to, tokenId, ""); + } + + /// @inheritdoc IERC721 + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override { + if (!_isApprovedOrOwner(msg.sender, tokenId)) { + revert POOL_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL(); + } + + _safeTransfer(from, to, tokenId, data); + } + + /// @notice Returns whether `spender` is allowed to manage `tokenId`. + /// @dev Will revert if `tokenId` doesn't exist. + /// @param spender the spender of the token + /// @param tokenId the id of the token to be spent + /// @return whether `tokenId` can be spent by `spender` + function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) { + address owner = PoolNFTBase.ownerOf(tokenId); + return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender); + } + + /// @notice Reverts if `tokenId` doesn't exist + /// @param tokenId the token id whose existence we are checking + function _requireMinted(uint256 tokenId) internal view { + if (!_exists(tokenId)) revert POOL_NFT_INVALID_TOKEN_ID(); + } + + /// @notice Returns whether `tokenId` exists + /// @dev Tokens can be managed by their owner or approved accounts via `approve` or `setApprovalForAll`. + /// Tokens start existing when they are minted (`_mint`), + /// and stop existing when they are burned (`_burn`). + /// @param tokenId the token id we're interested in seeing if exists + /// @return bool whether ot not the token exists + function _exists(uint256 tokenId) internal view returns (bool) { + return _ownerOf(tokenId) != address(0); + } + + function _triggerMetadataUpdate(uint256 tokenId) internal { + emit MetadataUpdate(tokenId); + } + + function _approve(address to, uint256 tokenId) internal { + _tokenApprovals[tokenId] = to; + + emit Approval(_ownerOf(tokenId), to, tokenId); + } + + function _setApprovalForAll(address owner, address operator, bool approved) internal { + if (owner == operator) revert POOL_NFT_APPROVE_TO_CALLER(); + + _operatorApprovals[owner][operator] = approved; + + emit ApprovalForAll(owner, operator, approved); + } + + /// @dev Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist. + /// @param tokenId the token id whose existence we're checking + /// @return address the address of the owner of `tokenId` + function _ownerOf(uint256 tokenId) internal view virtual returns (address); + + function _transfer(address from, address to, uint256 tokenId) internal virtual; + + function _safeTransfer( + address from, + address to, + uint256 tokenId, + bytes memory // data + ) internal virtual { + _transfer(from, to, tokenId); + } + + /// @dev Deletes the tokenApprovals for `tokenId` + /// @param tokenId the token id whose approvals we're clearing + function _burn(uint256 tokenId) internal virtual { + // clear approvals from the previous owner + delete _tokenApprovals[tokenId]; + } +} diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol new file mode 100644 index 0000000000..fcb5714a4a --- /dev/null +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -0,0 +1,488 @@ +// SPDX-License-Identifier: AGPLv3 +// solhint-disable not-rely-on-time +pragma solidity 0.8.19; + +// Notes: We use these interfaces in natspec documentation below, grep @inheritdoc +// solhint-disable-next-line no-unused-import +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { + BasicParticle, + SemanticMoney, + PDPoolMember, + PDPoolIndex, + PDPoolMemberMU, + Value, + Time, + FlowRate, + Unit +} from "@superfluid-finance/solidity-semantic-money/src/SemanticMoney.sol"; +import { ISuperfluid } from "../../interfaces/superfluid/ISuperfluid.sol"; +import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol"; +import { ISuperToken } from "../../interfaces/superfluid/ISuperToken.sol"; +import { ISuperfluidPool } from "../../interfaces/agreements/gdav1/ISuperfluidPool.sol"; +import { GeneralDistributionAgreementV1 } from "../../agreements/gdav1/GeneralDistributionAgreementV1.sol"; +import { BeaconProxiable } from "../../upgradability/BeaconProxiable.sol"; +import { IPoolMemberNFT } from "../../interfaces/agreements/gdav1/IPoolMemberNFT.sol"; +import { SafeGasLibrary } from "../../libs/SafeGasLibrary.sol"; + +/** + * @title SuperfluidPool + * @author Superfluid + * @notice A SuperfluidPool which can be used to distribute any SuperToken. + * @dev Because we are using uint128, uint256 doesn't work here. + */ +contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { + using SemanticMoney for BasicParticle; + using SafeCast for uint256; + using SafeCast for int256; + + GeneralDistributionAgreementV1 public immutable GDA; + + ISuperfluidToken public superToken; + address public admin; + PoolIndexData internal _index; + mapping(address => MemberData) internal _membersData; + + /// @dev This is a pseudo member, representing all the disconnected members + MemberData internal _disconnectedMembers; + + /// @dev owner => (spender => amount) + mapping(address => mapping(address => uint256)) internal _allowances; + + /// @inheritdoc ISuperfluidPool + bool public transferabilityForUnitsOwner; + + /// @inheritdoc ISuperfluidPool + bool public distributionFromAnyAddress; + + constructor(GeneralDistributionAgreementV1 gda) { + GDA = gda; + } + + function initialize( + address admin_, + ISuperfluidToken superToken_, + bool transferabilityForUnitsOwner_, + bool distributionFromAnyAddress_ + ) external initializer { + admin = admin_; + superToken = superToken_; + transferabilityForUnitsOwner = transferabilityForUnitsOwner_; + distributionFromAnyAddress = distributionFromAnyAddress_; + } + + function proxiableUUID() public pure override returns (bytes32) { + return keccak256("org.superfluid-finance.contracts.SuperfluidPool.implementation"); + } + + function getIndex() external view returns (PoolIndexData memory) { + return _index; + } + + /// @inheritdoc ISuperfluidPool + function getTotalUnits() external view override returns (uint128) { + return _getTotalUnits(); + } + + function _getTotalUnits() internal view returns (uint128) { + return _index.totalUnits; + } + + /// @inheritdoc IERC20 + function allowance(address owner, address spender) external view override returns (uint256) { + return _allowances[owner][spender]; + } + + /// @inheritdoc IERC20 + function approve(address spender, uint256 amount) external override returns (bool) { + _approve(msg.sender, spender, amount); + return true; + } + /// @inheritdoc ISuperfluidPool + + function increaseAllowance(address spender, uint256 addedValue) external returns (bool) { + _approve(msg.sender, spender, _allowances[msg.sender][spender] + addedValue); + return true; + } + /// @inheritdoc ISuperfluidPool + + function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool) { + _approve(msg.sender, spender, _allowances[msg.sender][spender] - subtractedValue); + return true; + } + + function _approve(address owner, address spender, uint256 amount) internal { + _allowances[owner][spender] = amount; + + emit Approval(owner, spender, amount); + } + + /// @dev Transfers `amount` units from `msg.sender` to `to` + function transfer(address to, uint256 amount) external override returns (bool) { + _transfer(msg.sender, to, amount); + + return true; + } + + /// @dev Transfers `amount` units from `from` to `to` + function transferFrom(address from, address to, uint256 amount) external override returns (bool) { + uint256 allowed = _allowances[from][msg.sender]; + + // if allowed - amount is negative, this reverts due to overflow + if (allowed != type(uint256).max) _allowances[from][msg.sender] = allowed - amount; + + _transfer(from, to, amount); + + return true; + } + + function _transfer(address from, address to, uint256 amount) internal { + if (!transferabilityForUnitsOwner) revert SUPERFLUID_POOL_TRANSFER_UNITS_NOT_ALLOWED(); + + uint128 fromUnitsBefore = _getUnits(from); + uint128 toUnitsBefore = _getUnits(to); + _updateMemberUnits(from, fromUnitsBefore - amount.toUint128()); + _updateMemberUnits(to, toUnitsBefore + amount.toUint128()); + // assert that the units are updated correctly for from and for to. + emit Transfer(from, to, amount); + } + + /// @notice Returns the total number of units for a pool + function totalSupply() external view override returns (uint256) { + return _getTotalUnits(); + } + + /// @inheritdoc ISuperfluidPool + function getTotalConnectedUnits() external view override returns (uint128) { + return _index.totalUnits - _disconnectedMembers.ownedUnits; + } + + /// @inheritdoc ISuperfluidPool + function getTotalDisconnectedUnits() external view override returns (uint128) { + return _disconnectedMembers.ownedUnits; + } + + /// @inheritdoc ISuperfluidPool + function getUnits(address memberAddr) external view override returns (uint128) { + return _getUnits(memberAddr); + } + + function _getUnits(address memberAddr) internal view returns (uint128) { + return _membersData[memberAddr].ownedUnits; + } + + /// @notice Returns the total number of units for an account for this pool + /// @dev Although the type is uint256, this can never be greater than type(int128).max + /// because the custom user type Unit is int128 in the SemanticMoney library + /// @param account The account to query + /// @return The total number of owned units of the account + function balanceOf(address account) external view override returns (uint256) { + return uint256(_getUnits(account)); + } + + /// @inheritdoc ISuperfluidPool + function getTotalFlowRate() external view override returns (int96) { + return _getTotalFlowRate(); + } + + function _getTotalFlowRate() internal view returns (int96) { + return (_index.wrappedFlowRate * uint256(_index.totalUnits).toInt256()).toInt96(); + } + + /// @inheritdoc ISuperfluidPool + function getTotalConnectedFlowRate() external view override returns (int96) { + return _getTotalFlowRate() - _getTotalDisconnectedFlowRate(); + } + + function _getTotalDisconnectedFlowRate() internal view returns (int96 flowRate) { + PDPoolIndex memory pdPoolIndex = poolIndexDataToPDPoolIndex(_index); + PDPoolMember memory disconnectedMembers = _memberDataToPDPoolMember(_disconnectedMembers); + + return int256(FlowRate.unwrap(pdPoolIndex.flow_rate_per_unit().mul(disconnectedMembers.owned_units))).toInt96(); + } + + /// @inheritdoc ISuperfluidPool + function getTotalDisconnectedFlowRate() external view override returns (int96 flowRate) { + return _getTotalDisconnectedFlowRate(); + } + + /// @inheritdoc ISuperfluidPool + function getDisconnectedBalance(uint32 time) external view override returns (int256 balance) { + PDPoolIndex memory pdPoolIndex = poolIndexDataToPDPoolIndex(_index); + PDPoolMember memory pdPoolMember = _memberDataToPDPoolMember(_disconnectedMembers); + return Value.unwrap(PDPoolMemberMU(pdPoolIndex, pdPoolMember).rtb(Time.wrap(time))); + } + + /// @inheritdoc ISuperfluidPool + function getMemberFlowRate(address memberAddr) external view override returns (int96) { + uint128 units = _getUnits(memberAddr); + if (units == 0) return 0; + // @note total units must never exceed type(int96).max + else return (_index.wrappedFlowRate * uint256(units).toInt256()).toInt96(); + } + + function _poolIndexDataToWrappedParticle(PoolIndexData memory data) + internal + pure + returns (BasicParticle memory wrappedParticle) + { + wrappedParticle = BasicParticle({ + _settled_at: Time.wrap(data.wrappedSettledAt), + _flow_rate: FlowRate.wrap(int128(data.wrappedFlowRate)), // upcast from int96 is safe + _settled_value: Value.wrap(data.wrappedSettledValue) + }); + } + + function poolIndexDataToPDPoolIndex(PoolIndexData memory data) + public + pure + returns (PDPoolIndex memory pdPoolIndex) + { + pdPoolIndex = PDPoolIndex({ + total_units: _toSemanticMoneyUnit(data.totalUnits), + _wrapped_particle: _poolIndexDataToWrappedParticle(data) + }); + } + + function _pdPoolIndexToPoolIndexData(PDPoolIndex memory pdPoolIndex) + internal + pure + returns (PoolIndexData memory data) + { + data = PoolIndexData({ + totalUnits: int256(Unit.unwrap(pdPoolIndex.total_units)).toUint256().toUint128(), + wrappedSettledAt: Time.unwrap(pdPoolIndex.settled_at()), + wrappedFlowRate: int256(FlowRate.unwrap(pdPoolIndex.flow_rate_per_unit())).toInt96(), + wrappedSettledValue: Value.unwrap(pdPoolIndex._wrapped_particle._settled_value) + }); + } + + function _memberDataToPDPoolMember(MemberData memory memberData) + internal + pure + returns (PDPoolMember memory pdPoolMember) + { + pdPoolMember = PDPoolMember({ + owned_units: _toSemanticMoneyUnit(memberData.ownedUnits), + _synced_particle: BasicParticle({ + _settled_at: Time.wrap(memberData.syncedSettledAt), + _flow_rate: FlowRate.wrap(int128(memberData.syncedFlowRate)), // upcast from int96 is safe + _settled_value: Value.wrap(memberData.syncedSettledValue) + }), + _settled_value: Value.wrap(memberData.settledValue) + }); + } + + function _toSemanticMoneyUnit(uint128 units) internal pure returns (Unit) { + // @note safe upcasting from uint128 to uint256 + // and use of safecast library for downcasting from uint256 to int128 + return Unit.wrap(uint256(units).toInt256().toInt128()); + } + + function _pdPoolMemberToMemberData(PDPoolMember memory pdPoolMember, int256 claimedValue) + internal + pure + returns (MemberData memory memberData) + { + memberData = MemberData({ + ownedUnits: uint256(int256(Unit.unwrap(pdPoolMember.owned_units))).toUint128(), + syncedSettledAt: Time.unwrap(pdPoolMember._synced_particle._settled_at), + syncedFlowRate: int256(FlowRate.unwrap(pdPoolMember._synced_particle._flow_rate)).toInt96(), + syncedSettledValue: Value.unwrap(pdPoolMember._synced_particle._settled_value), + settledValue: Value.unwrap(pdPoolMember._settled_value), + claimedValue: claimedValue + }); + } + + /// @inheritdoc ISuperfluidPool + function getClaimableNow(address memberAddr) + external + view + override + returns (int256 claimableBalance, uint256 timestamp) + { + timestamp = ISuperfluid(superToken.getHost()).getNow(); + return (getClaimable(memberAddr, uint32(timestamp)), timestamp); + } + + /// @inheritdoc ISuperfluidPool + function getClaimable(address memberAddr, uint32 time) public view override returns (int256) { + Time t = Time.wrap(time); + PDPoolIndex memory pdPoolIndex = poolIndexDataToPDPoolIndex(_index); + PDPoolMember memory pdPoolMember = _memberDataToPDPoolMember(_membersData[memberAddr]); + return Value.unwrap( + PDPoolMemberMU(pdPoolIndex, pdPoolMember).rtb(t) - Value.wrap(_membersData[memberAddr].claimedValue) + ); + } + + /// @inheritdoc ISuperfluidPool + function updateMemberUnits(address memberAddr, uint128 newUnits) external returns (bool) { + if (msg.sender != admin && msg.sender != address(GDA)) revert SUPERFLUID_POOL_NOT_POOL_ADMIN_OR_GDA(); + + _updateMemberUnits(memberAddr, newUnits); + + return true; + } + + /** + * @notice Checks whether or not the NFT hook can be called. + * @dev A staticcall, so `POOL_MEMBER_NFT` must be a view otherwise the assumption is that it reverts + * @param token the super token that is being streamed + * @return poolMemberNFT the address returned by low level call + */ + function _canCallNFTHook(ISuperfluidToken token) internal view returns (address poolMemberNFT) { + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory data) = + address(token).staticcall(abi.encodeWithSelector(ISuperToken.POOL_MEMBER_NFT.selector)); + + if (success) { + // @note We are aware this may revert if a Custom SuperToken's + // POOL_MEMBER_NFT does not return data that can be + // decoded to an address. This would mean it was intentionally + // done by the creator of the Custom SuperToken logic and is + // fully expected to revert in that case as the author desired. + poolMemberNFT = abi.decode(data, (address)); + } + } + + function _handlePoolMemberNFT(address memberAddr, uint128 newUnits) internal { + // Pool Member NFT Logic + IPoolMemberNFT poolMemberNFT = IPoolMemberNFT(_canCallNFTHook(superToken)); + if (address(poolMemberNFT) != address(0)) { + uint256 tokenId = poolMemberNFT.getTokenId(address(this), memberAddr); + uint256 gasLeftBefore; + if (newUnits == 0) { + if (poolMemberNFT.poolMemberDataByTokenId(tokenId).member != address(0)) { + gasLeftBefore = gasleft(); + // solhint-disable-next-line no-empty-blocks + try poolMemberNFT.onDelete(address(this), memberAddr) { } + catch { + SafeGasLibrary._revertWhenOutOfGas(gasLeftBefore); + } + } + } else { + // if not minted, we mint a new pool member nft + if (poolMemberNFT.poolMemberDataByTokenId(tokenId).member == address(0)) { + gasLeftBefore = gasleft(); + // solhint-disable-next-line no-empty-blocks + try poolMemberNFT.onCreate(address(this), memberAddr) { } + catch { + SafeGasLibrary._revertWhenOutOfGas(gasLeftBefore); + } + // if minted, we update the pool member nft + } else { + gasLeftBefore = gasleft(); + // solhint-disable-next-line no-empty-blocks + try poolMemberNFT.onUpdate(address(this), memberAddr) { } + catch { + SafeGasLibrary._revertWhenOutOfGas(gasLeftBefore); + } + } + } + } + } + + function _updateMemberUnits(address memberAddr, uint128 newUnits) internal returns (bool) { + // @note normally we keep the sanitization in the external functions, but here + // this is used in both updateMemberUnits and transfer + if (GDA.isPool(superToken, memberAddr)) revert SUPERFLUID_POOL_NO_POOL_MEMBERS(); + if (memberAddr == address(0)) revert SUPERFLUID_POOL_NO_ZERO_ADDRESS(); + + uint32 time = uint32(ISuperfluid(superToken.getHost()).getNow()); + Time t = Time.wrap(time); + Unit wrappedUnits = _toSemanticMoneyUnit(newUnits); + + PDPoolIndex memory pdPoolIndex = poolIndexDataToPDPoolIndex(_index); + MemberData memory memberData = _membersData[memberAddr]; + PDPoolMember memory pdPoolMember = _memberDataToPDPoolMember(memberData); + + uint128 oldUnits = memberData.ownedUnits; + + PDPoolMemberMU memory mu = PDPoolMemberMU(pdPoolIndex, pdPoolMember); + + // update pool's disconnected units + if (!GDA.isMemberConnected(ISuperfluidPool(address(this)), memberAddr)) { + // trigger the side effect of claiming all if not connected + // @note claiming is a bit surprising here given the function name + int256 claimedAmount = _claimAll(memberAddr, time); + + // update pool's disconnected units + _shiftDisconnectedUnits(wrappedUnits - mu.m.owned_units, Value.wrap(claimedAmount), t); + } + + // update pool member's units + { + BasicParticle memory p; + (pdPoolIndex, pdPoolMember, p) = mu.pool_member_update(p, wrappedUnits, t); + _index = _pdPoolIndexToPoolIndexData(pdPoolIndex); + int256 claimedValue = _membersData[memberAddr].claimedValue; + _membersData[memberAddr] = _pdPoolMemberToMemberData(pdPoolMember, claimedValue); + assert(GDA.appendIndexUpdateByPool(superToken, p, t)); + } + emit MemberUnitsUpdated(superToken, memberAddr, oldUnits, newUnits); + + _handlePoolMemberNFT(memberAddr, newUnits); + + return true; + } + + function _claimAll(address memberAddr, uint32 time) internal returns (int256 amount) { + amount = getClaimable(memberAddr, time); + assert(GDA.poolSettleClaim(superToken, memberAddr, (amount))); + _membersData[memberAddr].claimedValue += amount; + + emit DistributionClaimed(superToken, memberAddr, amount, _membersData[memberAddr].claimedValue); + } + + /// @inheritdoc ISuperfluidPool + function claimAll() external returns (bool) { + return claimAll(msg.sender); + } + + /// @inheritdoc ISuperfluidPool + function claimAll(address memberAddr) public returns (bool) { + bool isConnected = GDA.isMemberConnected(ISuperfluidPool(address(this)), memberAddr); + uint32 time = uint32(ISuperfluid(superToken.getHost()).getNow()); + int256 claimedAmount = _claimAll(memberAddr, time); + if (!isConnected) { + _shiftDisconnectedUnits(Unit.wrap(0), Value.wrap(claimedAmount), Time.wrap(time)); + } + + return true; + } + + function operatorSetIndex(PDPoolIndex calldata index) external onlyGDA returns (bool) { + _index = _pdPoolIndexToPoolIndexData(index); + + return true; + } + + // WARNING for operators: it is undefined behavior if member is already connected or disconnected + function operatorConnectMember(address memberAddr, bool doConnect, uint32 time) external onlyGDA returns (bool) { + int256 claimedAmount = _claimAll(memberAddr, time); + int128 units = uint256(_getUnits(memberAddr)).toInt256().toInt128(); + if (doConnect) { + _shiftDisconnectedUnits(Unit.wrap(-units), Value.wrap(claimedAmount), Time.wrap(time)); + } else { + _shiftDisconnectedUnits(Unit.wrap(units), Value.wrap(0), Time.wrap(time)); + } + return true; + } + + function _shiftDisconnectedUnits(Unit shiftUnits, Value claimedAmount, Time t) internal { + PDPoolIndex memory pdPoolIndex = poolIndexDataToPDPoolIndex(_index); + PDPoolMember memory disconnectedMembers = _memberDataToPDPoolMember(_disconnectedMembers); + PDPoolMemberMU memory mu = PDPoolMemberMU(pdPoolIndex, disconnectedMembers); + mu = mu.settle(t); + mu.m.owned_units = mu.m.owned_units + shiftUnits; + // offset the claimed amount from the settled value if any + mu.m._settled_value = mu.m._settled_value - claimedAmount; + _disconnectedMembers = _pdPoolMemberToMemberData(mu.m, 0); + } + + modifier onlyGDA() { + if (msg.sender != address(GDA)) revert SUPERFLUID_POOL_NOT_GDA(); + _; + } +} diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol new file mode 100644 index 0000000000..c3699a2c41 --- /dev/null +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol"; +import { SuperfluidPool } from "./SuperfluidPool.sol"; +import { PoolConfig } from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; + +library SuperfluidPoolDeployerLibrary { + function deploy( + address beacon, + address admin, + ISuperfluidToken token, + PoolConfig memory config + ) external returns (SuperfluidPool pool) { + bytes memory initializeCallData = abi.encodeWithSelector( + SuperfluidPool.initialize.selector, + admin, + token, + config.transferabilityForUnitsOwner, + config.distributionFromAnyAddress + ); + BeaconProxy superfluidPoolBeaconProxy = new BeaconProxy( + beacon, + initializeCallData + ); + pool = SuperfluidPool(address(superfluidPoolBeaconProxy)); + } +} diff --git a/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol b/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol index b749b8046d..df79a017ad 100644 --- a/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol +++ b/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol @@ -8,6 +8,12 @@ import { IInstantDistributionAgreementV1 } from "../interfaces/superfluid/ISuperfluid.sol"; +import { + IGeneralDistributionAgreementV1, + ISuperfluidPool, + PoolConfig +} from "../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; + /** * @title Library for Token Centric Interface * @author Superfluid @@ -819,18 +825,47 @@ library SuperTokenV1Library { } /** - * @dev get net flow rate for given account for given token + * @dev get net flow rate for given account for given token (CFA + GDA) * @param token Super token address * @param account Account to query * @return flowRate The net flow rate of the account */ function getNetFlowRate(ISuperToken token, address account) internal view returns (int96 flowRate) + { + (, IConstantFlowAgreementV1 cfa) = _getHostAndCFA(token); + (, IGeneralDistributionAgreementV1 gda) = _getHostAndGDA(token); + int96 cfaNetFlow = cfa.getNetFlow(token, account); + int96 gdaNetFlow = gda.getNetFlow(token, account); + return cfaNetFlow + gdaNetFlow; + } + + /** + * @dev get CFA net flow rate for given account for given token + * @param token Super token address + * @param account Account to query + * @return flowRate The net flow rate of the account + */ + function getCFANetFlowRate(ISuperToken token, address account) + internal view returns (int96 flowRate) { (, IConstantFlowAgreementV1 cfa) = _getHostAndCFA(token); return cfa.getNetFlow(token, account); } + /** + * @dev get GDA net flow rate for given account for given token + * @param token Super token address + * @param account Account to query + * @return flowRate The net flow rate of the account + */ + function getGDANetFlowRate(ISuperToken token, address account) + internal view returns (int96 flowRate) + { + (, IGeneralDistributionAgreementV1 gda) = _getHostAndGDA(token); + return gda.getNetFlow(token, account); + } + /** * @dev get the aggregated flow info of the account * @param token Super token address @@ -988,6 +1023,41 @@ library SuperTokenV1Library { return ida.getSubscriptionByID(token, agreementId); } + /** GDA VIEW FUNCTIONS ************************************* */ + function getFlowDistributionFlowRate(ISuperToken token, address from, ISuperfluidPool to) + internal + view + returns (int96) + { + (, IGeneralDistributionAgreementV1 gda) = _getHostAndGDA(token); + return gda.getFlowRate(token, from, to); + } + + function estimateFlowDistributionActualFlowRate( + ISuperToken token, + address from, + ISuperfluidPool to, + int96 requestedFlowRate + ) internal view returns (int96 actualFlowRate, int96 totalDistributionFlowRate) { + (, IGeneralDistributionAgreementV1 gda) = _getHostAndGDA(token); + return gda.estimateFlowDistributionActualFlowRate(token, from, to, requestedFlowRate); + } + + function estimateDistributionActualAmount( + ISuperToken token, + address from, + ISuperfluidPool to, + uint256 requestedAmount + ) internal view returns (uint256 actualAmount) { + (, IGeneralDistributionAgreementV1 gda) = _getHostAndGDA(token); + return gda.estimateDistributionActualAmount(token, from, to, requestedAmount); + } + + function isMemberConnected(ISuperToken token, address pool, address member) internal view returns (bool) { + (, IGeneralDistributionAgreementV1 gda) = _getHostAndGDA(token); + return gda.isMemberConnected(ISuperfluidPool(pool), member); + } + /** IDA BASE FUNCTIONS ************************************* */ @@ -1636,6 +1706,250 @@ library SuperTokenV1Library { ); } + /** GDA BASE FUNCTIONS ************************************* */ + + function createPool(ISuperToken token, address admin, PoolConfig memory poolConfig) + internal + returns (ISuperfluidPool pool) + { + (, IGeneralDistributionAgreementV1 gda) = _getAndCacheHostAndGDA(token); + pool = gda.createPool(token, admin, poolConfig); + } + + function updateMemberUnits(ISuperToken token, ISuperfluidPool pool, address memberAddress, uint128 newUnits) + internal + returns (bool) + { + return updateMemberUnits(token, pool, memberAddress, newUnits, new bytes(0)); + } + + function updateMemberUnits( + ISuperToken token, + ISuperfluidPool pool, + address memberAddress, + uint128 newUnits, + bytes memory userData + ) internal returns (bool) { + (ISuperfluid host, IGeneralDistributionAgreementV1 gda) = _getAndCacheHostAndGDA(token); + host.callAgreement( + gda, abi.encodeCall(gda.updateMemberUnits, (pool, memberAddress, newUnits, new bytes(0))), userData + ); + + return true; + } + + function claimAll(ISuperToken token, ISuperfluidPool pool, address memberAddress) internal returns (bool) { + return claimAll(token, pool, memberAddress, new bytes(0)); + } + + function claimAll(ISuperToken token, ISuperfluidPool pool, address memberAddress, bytes memory userData) + internal + returns (bool) + { + (ISuperfluid host, IGeneralDistributionAgreementV1 gda) = _getAndCacheHostAndGDA(token); + host.callAgreement(gda, abi.encodeCall(gda.claimAll, (pool, memberAddress, new bytes(0))), userData); + + return true; + } + + function connectPool(ISuperToken token, ISuperfluidPool pool) internal returns (bool) { + return connectPool(token, pool, new bytes(0)); + } + + function connectPool(ISuperToken token, ISuperfluidPool pool, bytes memory userData) internal returns (bool) { + (ISuperfluid host, IGeneralDistributionAgreementV1 gda) = _getAndCacheHostAndGDA(token); + host.callAgreement(gda, abi.encodeCall(gda.connectPool, (pool, new bytes(0))), userData); + + return true; + } + + function disconnectPool(ISuperToken token, ISuperfluidPool pool) internal returns (bool) { + return disconnectPool(token, pool, new bytes(0)); + } + + function disconnectPool(ISuperToken token, ISuperfluidPool pool, bytes memory userData) internal returns (bool) { + (ISuperfluid host, IGeneralDistributionAgreementV1 gda) = _getAndCacheHostAndGDA(token); + host.callAgreement(gda, abi.encodeCall(gda.disconnectPool, (pool, new bytes(0))), userData); + return true; + } + + // @note we already have a distribute function from IDA, do we want this too? do we want to differentiate this? + function distributeToPool(ISuperToken token, address from, ISuperfluidPool pool, uint256 requestedAmount) + internal + returns (bool) + { + return distribute(token, from, pool, requestedAmount, new bytes(0)); + } + + function distribute( + ISuperToken token, + address from, + ISuperfluidPool pool, + uint256 requestedAmount, + bytes memory userData + ) internal returns (bool) { + (ISuperfluid host, IGeneralDistributionAgreementV1 gda) = _getAndCacheHostAndGDA(token); + host.callAgreement( + gda, abi.encodeCall(gda.distribute, (token, from, pool, requestedAmount, new bytes(0))), userData + ); + return true; + } + + function distributeFlow(ISuperToken token, address from, ISuperfluidPool pool, int96 requestedFlowRate) + internal + returns (bool) + { + return distributeFlow(token, from, pool, requestedFlowRate, new bytes(0)); + } + + function distributeFlow( + ISuperToken token, + address from, + ISuperfluidPool pool, + int96 requestedFlowRate, + bytes memory userData + ) internal returns (bool) { + (ISuperfluid host, IGeneralDistributionAgreementV1 gda) = _getAndCacheHostAndGDA(token); + host.callAgreement( + gda, abi.encodeCall(gda.distributeFlow, (token, from, pool, requestedFlowRate, new bytes(0))), userData + ); + return true; + } + + /** GDA WITH CTX FUNCTIONS ************************************* */ + + function updateMemberUnitsWithCtx( + ISuperToken token, + ISuperfluidPool pool, + address memberAddress, + uint128 newUnits, + bytes memory ctx + ) internal returns (bytes memory newCtx) { + (ISuperfluid host, IGeneralDistributionAgreementV1 gda) = _getAndCacheHostAndGDA(token); + (newCtx,) = host.callAgreementWithContext( + gda, + abi.encodeCall( + gda.updateMemberUnits, + ( + pool, + memberAddress, + newUnits, + new bytes(0) // ctx placeholder + ) + ), + "0x", + ctx + ); + } + + function claimAllWithCtx(ISuperToken token, ISuperfluidPool pool, address memberAddress, bytes memory ctx) + internal + returns (bytes memory newCtx) + { + (ISuperfluid host, IGeneralDistributionAgreementV1 gda) = _getAndCacheHostAndGDA(token); + (newCtx,) = host.callAgreementWithContext( + gda, + abi.encodeCall( + gda.claimAll, + ( + pool, + memberAddress, + new bytes(0) // ctx placeholder + ) + ), + "0x", + ctx + ); + } + + function connectPoolWithCtx(ISuperToken token, ISuperfluidPool pool, bytes memory ctx) + internal + returns (bytes memory newCtx) + { + (ISuperfluid host, IGeneralDistributionAgreementV1 gda) = _getAndCacheHostAndGDA(token); + (newCtx,) = host.callAgreementWithContext( + gda, + abi.encodeCall( + gda.connectPool, + ( + pool, + new bytes(0) // ctx placeholder + ) + ), + "0x", + ctx + ); + } + + function disconnectPoolWithCtx(ISuperToken token, ISuperfluidPool pool, bytes memory ctx) + internal + returns (bytes memory newCtx) + { + (ISuperfluid host, IGeneralDistributionAgreementV1 gda) = _getAndCacheHostAndGDA(token); + (newCtx,) = host.callAgreementWithContext( + gda, + abi.encodeCall( + gda.disconnectPool, + ( + pool, + new bytes(0) // ctx placeholder + ) + ), + "0x", + ctx + ); + } + + function distributeWithCtx( + ISuperToken token, + ISuperfluidPool pool, + address from, + uint256 requestedAmount, + bytes memory ctx + ) internal returns (bytes memory newCtx) { + (ISuperfluid host, IGeneralDistributionAgreementV1 gda) = _getAndCacheHostAndGDA(token); + (newCtx,) = host.callAgreementWithContext( + gda, + abi.encodeCall( + gda.distribute, + ( + token, + from, + pool, + requestedAmount, + new bytes(0) // ctx placeholder + ) + ), + "0x", + ctx + ); + } + + function distributeFlowWithCtx( + ISuperToken token, + address from, + ISuperfluidPool pool, + int96 requestedFlowRate, + bytes memory ctx + ) internal returns (bytes memory newCtx) { + (ISuperfluid host, IGeneralDistributionAgreementV1 gda) = _getAndCacheHostAndGDA(token); + (newCtx,) = host.callAgreementWithContext( + gda, + abi.encodeCall( + gda.distributeFlow, + ( + token, + from, + pool, + requestedFlowRate, + new bytes(0) // ctx placeholder + ) + ), + "0x", + ctx + ); + } + // ************** private helpers ************** // @note We must use hardcoded constants here because: @@ -1646,14 +1960,18 @@ library SuperTokenV1Library { bytes32 private constant _CFA_SLOT = 0xb969d79d88acd02d04ed7ee7d43b949e7daf093d363abcfbbc43dfdfd1ce969a; // keccak256("org.superfluid-finance.apps.SuperTokenLibrary.v1.ida"); bytes32 private constant _IDA_SLOT = 0xa832ee1924ea960211af2df07d65d166232018f613ac6708043cd8f8773eddeb; + // keccak256("org.superfluid-finance.apps.SuperTokenLibrary.v1.gda"); + bytes32 private constant _GDA_SLOT = 0xc36f6c05164a669ecb6da53e218d77ae44d51cfc99f91e5a125a18de0949bee4; // gets the host and cfa addrs for the token and caches it in storage for gas efficiency // to be used in state changing methods - function _getAndCacheHostAndCFA(ISuperToken token) private - returns(ISuperfluid host, IConstantFlowAgreementV1 cfa) + function _getAndCacheHostAndCFA(ISuperToken token) + private + returns (ISuperfluid host, IConstantFlowAgreementV1 cfa) { // check if already in contract storage... - assembly { // solium-disable-line + assembly { + // solium-disable-line host := sload(_HOST_SLOT) cfa := sload(_CFA_SLOT) } @@ -1662,11 +1980,12 @@ library SuperTokenV1Library { if (address(host) == address(0)) { host = ISuperfluid(token.getHost()); } + cfa = IConstantFlowAgreementV1(address(ISuperfluid(host).getAgreementClass( keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1")))); // now that we got them and are in a transaction context, persist in storage assembly { - // solium-disable-line + // solium-disable-line sstore(_HOST_SLOT, host) sstore(_CFA_SLOT, cfa) } @@ -1677,11 +1996,13 @@ library SuperTokenV1Library { // gets the host and ida addrs for the token and caches it in storage for gas efficiency // to be used in state changing methods - function _getAndCacheHostAndIDA(ISuperToken token) private - returns(ISuperfluid host, IInstantDistributionAgreementV1 ida) + function _getAndCacheHostAndIDA(ISuperToken token) + private + returns (ISuperfluid host, IInstantDistributionAgreementV1 ida) { // check if already in contract storage... - assembly { // solium-disable-line + assembly { + // solium-disable-line host := sload(_HOST_SLOT) ida := sload(_IDA_SLOT) } @@ -1694,7 +2015,7 @@ library SuperTokenV1Library { keccak256("org.superfluid-finance.agreements.InstantDistributionAgreement.v1")))); // now that we got them and are in a transaction context, persist in storage assembly { - // solium-disable-line + // solium-disable-line sstore(_HOST_SLOT, host) sstore(_IDA_SLOT, ida) } @@ -1703,13 +2024,47 @@ library SuperTokenV1Library { assert(address(ida) != address(0)); } + // gets the host and gda addrs for the token and caches it in storage for gas efficiency + // to be used in state changing methods + function _getAndCacheHostAndGDA(ISuperToken token) + private + returns (ISuperfluid host, IGeneralDistributionAgreementV1 gda) + { + // check if already in contract storage... + assembly { + // solium-disable-line + host := sload(_HOST_SLOT) + gda := sload(_GDA_SLOT) + } + if (address(gda) == address(0)) { + // framework contract addrs not yet cached, retrieving now... + if (address(host) == address(0)) { + host = ISuperfluid(token.getHost()); + } + gda = IGeneralDistributionAgreementV1( + address( + ISuperfluid(host).getAgreementClass( + keccak256("org.superfluid-finance.agreements.GeneralDistributionAgreement.v1") + ) + ) + ); + // now that we got them and are in a transaction context, persist in storage + assembly { + // solium-disable-line + sstore(_HOST_SLOT, host) + sstore(_GDA_SLOT, gda) + } + } + assert(address(host) != address(0)); + assert(address(gda) != address(0)); + } + // gets the host and cfa addrs for the token // to be used in non-state changing methods (view functions) - function _getHostAndCFA(ISuperToken token) private view - returns(ISuperfluid host, IConstantFlowAgreementV1 cfa) - { + function _getHostAndCFA(ISuperToken token) private view returns (ISuperfluid host, IConstantFlowAgreementV1 cfa) { // check if already in contract storage... - assembly { // solium-disable-line + assembly { + // solium-disable-line host := sload(_HOST_SLOT) cfa := sload(_CFA_SLOT) } @@ -1727,11 +2082,14 @@ library SuperTokenV1Library { // gets the host and ida addrs for the token // to be used in non-state changing methods (view functions) - function _getHostAndIDA(ISuperToken token) private view - returns(ISuperfluid host, IInstantDistributionAgreementV1 ida) + function _getHostAndIDA(ISuperToken token) + private + view + returns (ISuperfluid host, IInstantDistributionAgreementV1 ida) { // check if already in contract storage... - assembly { // solium-disable-line + assembly { + // solium-disable-line host := sload(_HOST_SLOT) ida := sload(_IDA_SLOT) } @@ -1746,4 +2104,34 @@ library SuperTokenV1Library { assert(address(host) != address(0)); assert(address(ida) != address(0)); } + + // gets the host and gda addrs for the token + // to be used in non-state changing methods (view functions) + function _getHostAndGDA(ISuperToken token) + private + view + returns (ISuperfluid host, IGeneralDistributionAgreementV1 gda) + { + // check if already in contract storage... + assembly { + // solium-disable-line + host := sload(_HOST_SLOT) + gda := sload(_GDA_SLOT) + } + if (address(gda) == address(0)) { + // framework contract addrs not yet cached in storage, retrieving now... + if (address(host) == address(0)) { + host = ISuperfluid(token.getHost()); + } + gda = IGeneralDistributionAgreementV1( + address( + ISuperfluid(host).getAgreementClass( + keccak256("org.superfluid-finance.agreements.GeneralDistributionAgreement.v1") + ) + ) + ); + } + assert(address(host) != address(0)); + assert(address(gda) != address(0)); + } } diff --git a/packages/ethereum-contracts/contracts/gov/SuperfluidGovernanceBase.sol b/packages/ethereum-contracts/contracts/gov/SuperfluidGovernanceBase.sol index 3ed7e16750..4473fe2f18 100644 --- a/packages/ethereum-contracts/contracts/gov/SuperfluidGovernanceBase.sol +++ b/packages/ethereum-contracts/contracts/gov/SuperfluidGovernanceBase.sol @@ -64,7 +64,8 @@ abstract contract SuperfluidGovernanceBase is ISuperfluidGovernance ISuperfluid host, address hostNewLogic, address[] calldata agreementClassNewLogics, - address superTokenFactoryNewLogic + address superTokenFactoryNewLogic, + address poolBeaconNewLogic ) external override onlyAuthorized(host) @@ -87,6 +88,9 @@ abstract contract SuperfluidGovernanceBase is ISuperfluidGovernance // solhint-disable-next-line no-empty-blocks catch {} } + if (poolBeaconNewLogic != address(0)) { + host.updatePoolBeaconLogic(poolBeaconNewLogic); + } } function batchUpdateSuperTokenLogic( diff --git a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol new file mode 100644 index 0000000000..5b4e1fdb3b --- /dev/null +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity >=0.8.4; + +import { ISuperAgreement } from "../../superfluid/ISuperAgreement.sol"; +import { ISuperfluidToken } from "../../superfluid/ISuperfluidToken.sol"; +import { ISuperfluidPool } from "../../agreements/gdav1/ISuperfluidPool.sol"; + +struct PoolConfig { + /// @dev if true, the pool members can transfer their owned units + /// else, only the pool admin can manipulate the units for pool members + bool transferabilityForUnitsOwner; + /// @dev if true, anyone can execute distributions via the pool + /// else, only the pool admin can execute distributions via the pool + bool distributionFromAnyAddress; +} + +/** + * @title General Distribution Agreement interface + * @author Superfluid + */ +abstract contract IGeneralDistributionAgreementV1 is ISuperAgreement { + // Structs + struct UniversalIndexData { + int96 flowRate; + uint32 settledAt; + uint256 totalBuffer; + bool isPool; + int256 settledValue; + } + + struct FlowDistributionData { + uint32 lastUpdated; + int96 flowRate; + uint256 buffer; // stored as uint96 + } + + struct PoolMemberData { + address pool; + uint32 poolID; // the slot id in the pool's subs bitmap + } + + struct StackVarsLiquidation { + ISuperfluidToken token; + int256 availableBalance; + address sender; + bytes32 distributionFlowHash; + int256 signedTotalGDADeposit; + address liquidator; + } + + + // Custom Errors + error GDA_DISTRIBUTE_FOR_OTHERS_NOT_ALLOWED(); // 0xf67d263e + error GDA_DISTRIBUTE_FROM_ANY_ADDRESS_NOT_ALLOWED(); // 0x7761a5e5 + error GDA_FLOW_DOES_NOT_EXIST(); // 0x29f4697e + error GDA_NON_CRITICAL_SENDER(); // 0x666f381d + error GDA_INSUFFICIENT_BALANCE(); // 0x33115c3f + error GDA_NO_NEGATIVE_FLOW_RATE(); // 0x15f25663 + error GDA_ADMIN_CANNOT_BE_POOL(); // 0x9ab88a26 + error GDA_NOT_POOL_ADMIN(); // 0x3a87e565 + error GDA_NO_ZERO_ADDRESS_ADMIN(); // 0x82c5d837 + error GDA_ONLY_SUPER_TOKEN_POOL(); // 0x90028c37 + + + // Events + event InstantDistributionUpdated( + ISuperfluidToken indexed token, + ISuperfluidPool indexed pool, + address indexed distributor, + address operator, + uint256 requestedAmount, + uint256 actualAmount, + bytes userData + ); + + event FlowDistributionUpdated( + ISuperfluidToken indexed token, + ISuperfluidPool indexed pool, + address indexed distributor, + // operator's have permission to liquidate critical flows + // on behalf of others + address operator, + int96 oldFlowRate, + int96 newDistributorToPoolFlowRate, + int96 newTotalDistributionFlowRate, + address adjustmentFlowRecipient, + int96 adjustmentFlowRate, + bytes userData + ); + + event PoolCreated(ISuperfluidToken indexed token, address indexed admin, ISuperfluidPool pool); + + event PoolConnectionUpdated( + ISuperfluidToken indexed token, + ISuperfluidPool indexed pool, + address indexed account, + bool connected, + bytes userData + ); + + event BufferAdjusted( + ISuperfluidToken indexed token, + ISuperfluidPool indexed pool, + address indexed from, + int256 bufferDelta, + uint256 newBufferAmount, + uint256 totalBufferAmount + ); + + /// @dev ISuperAgreement.agreementType implementation + function agreementType() external pure override returns (bytes32) { + return keccak256("org.superfluid-finance.agreements.GeneralDistributionAgreement.v1"); + } + + /// @dev Gets the GDA net flow rate of `account` for `token`. + /// @param token The token address + /// @param account The account address + /// @return net flow rate + function getNetFlow(ISuperfluidToken token, address account) external view virtual returns (int96); + + /// @notice Gets the GDA flow rate of `from` to `to` for `token`. + /// @dev This is primarily used to get the flow distribution flow rate from a distributor to a pool or the + /// adjustment flow rate of a pool. + /// @param token The token address + /// @param from The sender address + /// @param to The receiver address (the pool) + /// @return flow rate + function getFlowRate(ISuperfluidToken token, address from, ISuperfluidPool to) + external + view + virtual + returns (int96); + + /// @notice Executes an optimistic estimation of what the actual flow distribution flow rate may be. + /// The actual flow distribution flow rate is the flow rate that will be sent from `from`. + /// NOTE: this is only precise in an atomic transaction. DO NOT rely on this if querying off-chain. + /// @dev The difference between the requested flow rate and the actual flow rate is the adjustment flow rate, + /// this adjustment flow rate goes to the pool admin. + /// @param token The token address + /// @param from The sender address + /// @param to The pool address + /// @param requestedFlowRate The requested flow rate + /// @return actualFlowRate and totalDistributionFlowRate + function estimateFlowDistributionActualFlowRate( + ISuperfluidToken token, + address from, + ISuperfluidPool to, + int96 requestedFlowRate + ) external view virtual returns (int96 actualFlowRate, int96 totalDistributionFlowRate); + + /// @notice Executes an optimistic estimation of what the actual amount distributed may be. + /// The actual amount distributed is the amount that will be sent from `from`. + /// NOTE: this is only precise in an atomic transaction. DO NOT rely on this if querying off-chain. + /// @dev The difference between the requested amount and the actual amount is the adjustment amount. + /// @param token The token address + /// @param from The sender address + /// @param to The pool address + /// @param requestedAmount The requested amount + /// @return actualAmount + function estimateDistributionActualAmount( + ISuperfluidToken token, + address from, + ISuperfluidPool to, + uint256 requestedAmount + ) external view virtual returns (uint256 actualAmount); + + /// @notice Gets the adjustment flow rate of `pool` for `token`. + /// @param pool The pool address + /// @return adjustment flow rate + function getPoolAdjustmentFlowRate(address pool) external view virtual returns (int96); + + //////////////////////////////////////////////////////////////////////////////// + // Pool Operations + //////////////////////////////////////////////////////////////////////////////// + + /// @notice Creates a new pool for `token` where the admin is `admin`. + /// @param token The token address + /// @param admin The admin of the pool + /// @param poolConfig The pool configuration (see PoolConfig struct) + function createPool(ISuperfluidToken token, address admin, PoolConfig memory poolConfig) + external + virtual + returns (ISuperfluidPool pool); + + function updateMemberUnits(ISuperfluidPool pool, address memberAddress, uint128 newUnits, bytes calldata ctx) + external + virtual + returns (bytes memory newCtx); + + function claimAll(ISuperfluidPool pool, address memberAddress, bytes calldata ctx) + external + virtual + returns (bytes memory newCtx); + + /// @notice Connects `msg.sender` to `pool`. + /// @dev This is used to connect a pool to the GDA. + /// @param pool The pool address + /// @param ctx Context bytes (see ISuperfluid.sol for Context struct) + /// @return newCtx the new context bytes + function connectPool(ISuperfluidPool pool, bytes calldata ctx) external virtual returns (bytes memory newCtx); + + /// @notice Disconnects `msg.sender` from `pool`. + /// @dev This is used to disconnect a pool from the GDA. + /// @param pool The pool address + /// @param ctx Context bytes (see ISuperfluidPoolAdmin for Context struct) + /// @return newCtx the new context bytes + function disconnectPool(ISuperfluidPool pool, bytes calldata ctx) external virtual returns (bytes memory newCtx); + + /// @notice Checks whether `account` is a pool. + /// @param token The token address + /// @param account The account address + /// @return true if `account` is a pool + function isPool(ISuperfluidToken token, address account) external view virtual returns (bool); + + /// Check if an address is connected to the pool + function isMemberConnected(ISuperfluidPool pool, address memberAddr) external view virtual returns (bool); + + /// Get pool adjustment flow information: (recipient, flowHash, flowRate) + function getPoolAdjustmentFlowInfo(ISuperfluidPool pool) external view virtual returns (address, bytes32, int96); + + //////////////////////////////////////////////////////////////////////////////// + // Agreement Operations + //////////////////////////////////////////////////////////////////////////////// + + /// @notice Tries to distribute `requestedAmount` of `token` from `from` to `pool`. + /// @dev NOTE: The actual amount distributed may differ. + /// @param token The token address + /// @param from The sender address + /// @param pool The pool address + /// @param requestedAmount The requested amount + /// @param ctx Context bytes (see ISuperfluidPool for Context struct) + /// @return newCtx the new context bytes + function distribute( + ISuperfluidToken token, + address from, + ISuperfluidPool pool, + uint256 requestedAmount, + bytes calldata ctx + ) external virtual returns (bytes memory newCtx); + + /// @notice Tries to distributeFlow `requestedFlowRate` of `token` from `from` to `pool`. + /// @dev NOTE: The actual distribution flow rate may differ. + /// @param token The token address + /// @param from The sender address + /// @param pool The pool address + /// @param requestedFlowRate The requested flow rate + /// @param ctx Context bytes (see ISuperfluidPool for Context struct) + /// @return newCtx the new context bytes + function distributeFlow( + ISuperfluidToken token, + address from, + ISuperfluidPool pool, + int96 requestedFlowRate, + bytes calldata ctx + ) external virtual returns (bytes memory newCtx); + + //////////////////////////////////////////////////////////////////////////////// + // Solvency Functions + //////////////////////////////////////////////////////////////////////////////// + + /** + * @dev Returns whether it is the patrician period based on host.getNow() + * @param account The account we are interested in + * @return isCurrentlyPatricianPeriod Whether it is currently the patrician period dictated by governance + * @return timestamp The value of host.getNow() + */ + function isPatricianPeriodNow(ISuperfluidToken token, address account) + external + view + virtual + returns (bool isCurrentlyPatricianPeriod, uint256 timestamp); + + /** + * @dev Returns whether it is the patrician period based on timestamp + * @param account The account we are interested in + * @param timestamp The timestamp we are interested in observing the result of isPatricianPeriod + * @return bool Whether it is currently the patrician period dictated by governance + */ + function isPatricianPeriod(ISuperfluidToken token, address account, uint256 timestamp) + public + view + virtual + returns (bool); +} diff --git a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolAdminNFT.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolAdminNFT.sol new file mode 100644 index 0000000000..f9a760a037 --- /dev/null +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolAdminNFT.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.11; + +import { IPoolNFTBase } from "./IPoolNFTBase.sol"; + +interface IPoolAdminNFT is IPoolNFTBase { + // PoolAdminNFTData struct storage packing: + // b = bits + // WORD 1: | pool | FREE + // | 160b | 96b + // WORD 2: | admin | FREE + // | 160b | 96b + struct PoolAdminNFTData { + address pool; + address admin; + } + + /// Write Functions /// + function mint(address pool) external; + + function poolAdminDataByTokenId(uint256 tokenId) external view returns (PoolAdminNFTData memory data); +} \ No newline at end of file diff --git a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol new file mode 100644 index 0000000000..92058adb03 --- /dev/null +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.11; + +import { IPoolNFTBase } from "./IPoolNFTBase.sol"; + +interface IPoolMemberNFT is IPoolNFTBase { + // PoolMemberNFTData struct storage packing: + // b = bits + // WORD 1: | pool | FREE + // | 160b | 96b + // WORD 2: | member | FREE + // | 160b | 96b + // WORD 3: | units | FREE + // | 128b | 128b + struct PoolMemberNFTData { + address pool; + address member; + uint128 units; + } + + /// Errors /// + + error POOL_MEMBER_NFT_NO_ZERO_POOL(); + error POOL_MEMBER_NFT_NO_ZERO_MEMBER(); + error POOL_MEMBER_NFT_NO_UNITS(); + error POOL_MEMBER_NFT_HAS_UNITS(); + + function onCreate(address pool, address member) external; + + function onUpdate(address pool, address member) external; + + function onDelete(address pool, address member) external; + + /// View Functions /// + + function poolMemberDataByTokenId(uint256 tokenId) external view returns (PoolMemberNFTData memory data); +} \ No newline at end of file diff --git a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolNFTBase.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolNFTBase.sol new file mode 100644 index 0000000000..587c7c97d5 --- /dev/null +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolNFTBase.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity >=0.8.4; + +import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; + +interface IPoolNFTBase is IERC721Metadata { + error POOL_NFT_APPROVE_TO_CALLER(); // 0x9212b333 + error POOL_NFT_ONLY_SUPER_TOKEN_FACTORY(); // 0x1fd7e3d8 + error POOL_NFT_INVALID_TOKEN_ID(); // 0x09275994 + error POOL_NFT_APPROVE_TO_CURRENT_OWNER(); // 0x020226d3 + error POOL_NFT_APPROVE_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL(); // 0x1e82f255 + error POOL_NFT_NOT_REGISTERED_POOL(); // 0x6421912e + error POOL_NFT_TRANSFER_NOT_ALLOWED(); // 0x432fb160 + error POOL_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL(); // 0x4028ee0e + + /// @notice Informs third-party platforms that NFT metadata should be updated + /// @dev This event comes from https://eips.ethereum.org/EIPS/eip-4906 + /// @param tokenId the id of the token that should have its metadata updated + event MetadataUpdate(uint256 tokenId); + + function initialize(string memory nftName, string memory nftSymbol) external; // initializer; + + function triggerMetadataUpdate(uint256 tokenId) external; + + /// @notice Gets the token id + /// @dev For PoolAdminNFT, `account` is admin and for PoolMemberNFT, `account` is member + function getTokenId(address pool, address account) external view returns (uint256 tokenId); +} diff --git a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol new file mode 100644 index 0000000000..4dee5ca426 --- /dev/null +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity >=0.8.4; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ISuperfluidToken } from "../../superfluid/ISuperfluidToken.sol"; + +/** + * @dev The interface for any super token pool regardless of the distribution schemes. + */ +interface ISuperfluidPool is IERC20 { + + // Structs + struct PoolIndexData { + uint128 totalUnits; + uint32 wrappedSettledAt; + int96 wrappedFlowRate; + int256 wrappedSettledValue; + } + + struct MemberData { + uint128 ownedUnits; + uint32 syncedSettledAt; + int96 syncedFlowRate; + int256 syncedSettledValue; + int256 settledValue; + int256 claimedValue; + } + + // Custom Errors + + error SUPERFLUID_POOL_INVALID_TIME(); // 0x83c35016 + error SUPERFLUID_POOL_NO_POOL_MEMBERS(); // 0xe10f405a + error SUPERFLUID_POOL_NO_ZERO_ADDRESS(); // 0x54eb6ee6 + error SUPERFLUID_POOL_NOT_POOL_ADMIN_OR_GDA(); // 0x1c5fbdcb + error SUPERFLUID_POOL_NOT_GDA(); // 0xfcbe3f9e + error SUPERFLUID_POOL_TRANSFER_UNITS_NOT_ALLOWED(); // 0x2285efba + + // Events + event MemberUnitsUpdated( + ISuperfluidToken indexed token, address indexed member, uint128 oldUnits, uint128 newUnits + ); + event DistributionClaimed( + ISuperfluidToken indexed token, address indexed member, int256 claimedAmount, int256 totalClaimed + ); + + /// @notice A boolean indicating whether pool members can transfer their units + function transferabilityForUnitsOwner() external view returns (bool); + + /// @notice A boolean indicating whether addresses other than the pool admin can distribute via the pool + function distributionFromAnyAddress() external view returns (bool); + + /// @notice The pool admin + /// @dev The admin is the creator of the pool and has permissions to update member units + /// and is the recipient of the adjustment flow rate + function admin() external view returns (address); + + /// @notice The SuperToken for the pool + function superToken() external view returns (ISuperfluidToken); + + /// @notice The total units of the pool + function getTotalUnits() external view returns (uint128); + + /// @notice The total number of units of connected members + function getTotalConnectedUnits() external view returns (uint128); + + /// @notice The total number of units of disconnected members + function getTotalDisconnectedUnits() external view returns (uint128); + + /// @notice The total number of units for `memberAddress` + /// @param memberAddress The address of the member + function getUnits(address memberAddress) external view returns (uint128); + + /// @notice The total flow rate of the pool + function getTotalFlowRate() external view returns (int96); + + /// @notice The flow rate of the connected members + function getTotalConnectedFlowRate() external view returns (int96); + + /// @notice The flow rate of the disconnected members + function getTotalDisconnectedFlowRate() external view returns (int96); + + /// @notice The balance of all the disconnected members at `time` + /// @param time The time to query + function getDisconnectedBalance(uint32 time) external view returns (int256 balance); + + /// @notice The flow rate a member is receiving from the pool + /// @param memberAddress The address of the member + function getMemberFlowRate(address memberAddress) external view returns (int96); + + /// @notice The claimable balance for `memberAddr` at `time` in the pool + /// @param memberAddr The address of the member + /// @param time The time to query + function getClaimable(address memberAddr, uint32 time) external view returns (int256); + + /// @notice The claimable balance for `memberAddr` at `block.timestamp` in the pool + /// @param memberAddr The address of the member + function getClaimableNow(address memberAddr) external view returns (int256 claimableBalance, uint256 timestamp); + + /// @notice Sets `memberAddr` ownedUnits to `newUnits` + /// @param memberAddr The address of the member + /// @param newUnits The new units for the member + function updateMemberUnits(address memberAddr, uint128 newUnits) external returns (bool); + + /// @notice Claims the claimable balance for `memberAddr` at `block.timestamp` + /// @param memberAddr The address of the member + function claimAll(address memberAddr) external returns (bool); + + /// @notice Claims the claimable balance for `msg.sender` at `block.timestamp` + function claimAll() external returns (bool); + + /// @notice Increases the allowance of `spender` by `addedValue` + /// @param spender The address of the spender + /// @param addedValue The amount to increase the allowance by + /// @return true if successful + function increaseAllowance(address spender, uint256 addedValue) external returns (bool); + + /// @notice Decreases the allowance of `spender` by `subtractedValue` + /// @param spender The address of the spender + /// @param subtractedValue The amount to decrease the allowance by + /// @return true if successful + function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool); +} diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/IPoolAdminNFT.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/IPoolAdminNFT.sol deleted file mode 100644 index 1475902cb6..0000000000 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/IPoolAdminNFT.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.11; - -// TODO -// solhint-disable-next-line no-empty-blocks -interface IPoolAdminNFT {} diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/IPoolMemberNFT.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/IPoolMemberNFT.sol deleted file mode 100644 index bf6776fb7c..0000000000 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/IPoolMemberNFT.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.11; - -// TODO -// solhint-disable-next-line no-empty-blocks -interface IPoolMemberNFT {} diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol index ab588f7106..ef62a0e93c 100644 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol +++ b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol @@ -6,6 +6,8 @@ import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/exte import { IERC777 } from "@openzeppelin/contracts/token/ERC777/IERC777.sol"; import { IConstantOutflowNFT } from "./IConstantOutflowNFT.sol"; import { IConstantInflowNFT } from "./IConstantInflowNFT.sol"; +import { IPoolAdminNFT } from "../agreements/gdav1/IPoolAdminNFT.sol"; +import { IPoolMemberNFT } from "../agreements/gdav1/IPoolMemberNFT.sol"; /** * @title Super token (Superfluid Token + ERC20 + ERC777) interface @@ -75,6 +77,10 @@ interface ISuperToken is ISuperfluidToken, IERC20Metadata, IERC777 { function CONSTANT_OUTFLOW_NFT() external view returns (IConstantOutflowNFT); // solhint-disable-next-line func-name-mixedcase function CONSTANT_INFLOW_NFT() external view returns (IConstantInflowNFT); + // solhint-disable-next-line func-name-mixedcase + function POOL_ADMIN_NFT() external view returns (IPoolAdminNFT); + // solhint-disable-next-line func-name-mixedcase + function POOL_MEMBER_NFT() external view returns (IPoolMemberNFT); /************************************************************************** * IERC20Metadata & ERC777 @@ -589,6 +595,22 @@ interface ISuperToken is ISuperfluidToken, IERC20Metadata, IERC777 { IConstantInflowNFT indexed constantInflowNFT ); + /** + * @dev Pool Admin NFT proxy created event + * @param poolAdminNFT pool admin nft address + */ + event PoolAdminNFTCreated( + IPoolAdminNFT indexed poolAdminNFT + ); + + /** + * @dev Pool Member NFT proxy created event + * @param poolMemberNFT pool member nft address + */ + event PoolMemberNFTCreated( + IPoolMemberNFT indexed poolMemberNFT + ); + /************************************************************************** * Function modifiers for access control and parameter validations * diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperTokenFactory.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperTokenFactory.sol index 16d40c13f1..9796eb6240 100644 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperTokenFactory.sol +++ b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperTokenFactory.sol @@ -19,10 +19,6 @@ interface ISuperTokenFactory { error SUPER_TOKEN_FACTORY_NON_UPGRADEABLE_IS_DEPRECATED(); // 0xc4901a43 error SUPER_TOKEN_FACTORY_ZERO_ADDRESS(); // 0x305c9e82 - /************************************************************************** - * Immutable Variables - **************************************************************************/ - /** * @dev Get superfluid host contract address */ diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol index 2137ccdcf6..b6abefb953 100644 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol +++ b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol @@ -26,12 +26,14 @@ import { ISETH } from "../tokens/ISETH.sol"; import { IFlowNFTBase } from "./IFlowNFTBase.sol"; import { IConstantOutflowNFT } from "./IConstantOutflowNFT.sol"; import { IConstantInflowNFT } from "./IConstantInflowNFT.sol"; -import { IPoolAdminNFT } from "./IPoolAdminNFT.sol"; -import { IPoolMemberNFT } from "./IPoolMemberNFT.sol"; +import { IPoolAdminNFT } from "../agreements/gdav1/IPoolAdminNFT.sol"; +import { IPoolMemberNFT } from "../agreements/gdav1/IPoolMemberNFT.sol"; /// Superfluid agreement interfaces: import { ISuperAgreement } from "./ISuperAgreement.sol"; import { IConstantFlowAgreementV1 } from "../agreements/IConstantFlowAgreementV1.sol"; import { IInstantDistributionAgreementV1 } from "../agreements/IInstantDistributionAgreementV1.sol"; +import { IGeneralDistributionAgreementV1, PoolConfig } from "../agreements/gdav1/IGeneralDistributionAgreementV1.sol"; +import { ISuperfluidPool } from "../agreements/gdav1/ISuperfluidPool.sol"; /// Superfluid App interfaces: import { ISuperApp } from "./ISuperApp.sol"; /// Superfluid governance @@ -238,6 +240,19 @@ interface ISuperfluid { */ function changeSuperTokenAdmin(ISuperToken token, address newAdmin) external; + /** + * @notice Change the implementation address the pool beacon points to + * @dev Updating the logic the beacon points to will update the logic of all the Pool BeaconProxy instances + */ + function updatePoolBeaconLogic(address newBeaconLogic) external; + + /** + * @dev Pool Beacon logic updated event + * @param beaconProxy addrss of the beacon proxy + * @param newBeaconLogic address of the new beacon logic + */ + event PoolBeaconLogicUpdated(address indexed beaconProxy, address newBeaconLogic); + /************************************************************************** * App Registry *************************************************************************/ diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluidGovernance.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluidGovernance.sol index 6ef45d6f79..0aa7e90b13 100644 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluidGovernance.sol +++ b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluidGovernance.sol @@ -41,7 +41,8 @@ interface ISuperfluidGovernance { ISuperfluid host, address hostNewLogic, address[] calldata agreementClassNewLogics, - address superTokenFactoryNewLogic + address superTokenFactoryNewLogic, + address beaconNewLogic ) external; /** diff --git a/packages/ethereum-contracts/contracts/libs/SafeGasLibrary.sol b/packages/ethereum-contracts/contracts/libs/SafeGasLibrary.sol index eedfd60f4e..33e24bb4b8 100644 --- a/packages/ethereum-contracts/contracts/libs/SafeGasLibrary.sol +++ b/packages/ethereum-contracts/contracts/libs/SafeGasLibrary.sol @@ -8,7 +8,7 @@ library SafeGasLibrary { error OUT_OF_GAS(); // 0x20afada5 function _isOutOfGas(uint256 gasLeftBefore) internal view returns (bool) { - return gasleft() <= gasLeftBefore / 63; + return gasleft() <= gasLeftBefore / 64; } /// @dev A function used in the catch block to handle true out of gas errors diff --git a/packages/ethereum-contracts/contracts/libs/SolvencyHelperLibrary.sol b/packages/ethereum-contracts/contracts/libs/SolvencyHelperLibrary.sol new file mode 100644 index 0000000000..33682ee75c --- /dev/null +++ b/packages/ethereum-contracts/contracts/libs/SolvencyHelperLibrary.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: AGPLv3 + +import { + ISuperfluid, + ISuperfluidToken, + ISuperfluidGovernance, + SuperfluidGovernanceConfigs +} from "../interfaces/superfluid/ISuperfluid.sol"; + +pragma solidity 0.8.19; + +library SolvencyHelperLibrary { + function decode3PsData(ISuperfluid host, ISuperfluidToken token) + internal + view + returns (uint256 liquidationPeriod, uint256 patricianPeriod) + { + ISuperfluidGovernance gov = ISuperfluidGovernance(host.getGovernance()); + // @note we are explicitly using CFAV1_PPP_CONFIG_KEY for both CFA and GDA + uint256 pppConfig = gov.getConfigAsUint256(host, token, SuperfluidGovernanceConfigs.CFAV1_PPP_CONFIG_KEY); + (liquidationPeriod, patricianPeriod) = SuperfluidGovernanceConfigs.decodePPPConfig(pppConfig); + } + + function isPatricianPeriod( + int256 availableBalance, + int256 signedTotalDeposit, + uint256 liquidationPeriod, + uint256 patricianPeriod + ) internal pure returns (bool) { + if (signedTotalDeposit == 0) { + return false; + } + + int256 totalRewardLeft = availableBalance + signedTotalDeposit; + int256 totalOutflowRate = signedTotalDeposit / int256(liquidationPeriod); + + return totalRewardLeft / totalOutflowRate > int256(liquidationPeriod - patricianPeriod); + } +} diff --git a/packages/ethereum-contracts/contracts/mocks/CFAv1NFTMock.sol b/packages/ethereum-contracts/contracts/mocks/CFAv1NFTMock.sol index 26b6c2c869..b9a1858aae 100644 --- a/packages/ethereum-contracts/contracts/mocks/CFAv1NFTMock.sol +++ b/packages/ethereum-contracts/contracts/mocks/CFAv1NFTMock.sol @@ -1,29 +1,66 @@ // SPDX-License-Identifier: AGPLv3 // solhint-disable reason-string +// solhint-disable not-rely-on-time pragma solidity 0.8.19; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { UUPSProxiable } from "../upgradability/UUPSProxiable.sol"; -import { ERC777Helper } from "../libs/ERC777Helper.sol"; -import { SuperfluidToken } from "../superfluid/SuperfluidToken.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { ISuperfluid, IConstantInflowNFT, IConstantOutflowNFT } from "../interfaces/superfluid/ISuperfluid.sol"; -import { ConstantInflowNFT } from "../superfluid/ConstantInflowNFT.sol"; import { ConstantOutflowNFT } from "../superfluid/ConstantOutflowNFT.sol"; +import { ConstantInflowNFT } from "../superfluid/ConstantInflowNFT.sol"; +import { FlowNFTBase } from "../superfluid/FlowNFTBase.sol"; + +/// @title FlowNFTBaseMock +/// @author Superfluid +/// @dev A mock contract for testing the functionality on FlowNFTBase +contract FlowNFTBaseMock is FlowNFTBase { + using Strings for uint256; + + mapping(uint256 => FlowNFTData) internal _flowDataByTokenId; + + constructor(ISuperfluid host) FlowNFTBase(host) { } + + function proxiableUUID() public pure override returns (bytes32) { + return keccak256("org.superfluid-finance.contracts.FlowNFTBaseMock.implementation"); + } + + /// @dev The owner of here is always the flow sender + function _ownerOf(uint256 tokenId) internal view override returns (address) { + return _flowDataByTokenId[tokenId].flowSender; + } + + /// @dev a mock mint function that sets the FlowNFTData + function mockMint(address _superToken, address _flowSender, address _flowReceiver) public { + uint256 tokenId = _getTokenId(_superToken, _flowSender, _flowReceiver); + _flowDataByTokenId[tokenId] = FlowNFTData({ + flowSender: _flowSender, + flowStartDate: uint32(block.timestamp), + flowReceiver: _flowReceiver, + superToken: _superToken + }); + } + + function _transfer( + address, //from, + address, //to, + uint256 //tokenId + ) internal pure override { + revert CFA_NFT_TRANSFER_IS_NOT_ALLOWED(); + } + + function flowDataByTokenId(uint256 tokenId) public view override returns (FlowNFTData memory flowData) { + return _flowDataByTokenId[tokenId]; + } + + function tokenURI(uint256 tokenId) external pure override returns (string memory) { + return string(abi.encodePacked("tokenId=", tokenId.toString())); + } +} contract ConstantOutflowNFTMock is ConstantOutflowNFT { - constructor( - ISuperfluid host, - IConstantInflowNFT constantInflowNFT - ) ConstantOutflowNFT(host, constantInflowNFT) {} + constructor(ISuperfluid host, IConstantInflowNFT constantInflowNFT) ConstantOutflowNFT(host, constantInflowNFT) { } /// @dev a mock mint function that exposes the internal _mint function - function mockMint( - address _superToken, - address _to, - address _flowReceiver, - uint256 _newTokenId - ) public { + function mockMint(address _superToken, address _to, address _flowReceiver, uint256 _newTokenId) public { _mint(_superToken, _to, _flowReceiver, _newTokenId); } @@ -47,7 +84,7 @@ contract ConstantInflowNFTMock is ConstantInflowNFT { constructor( ISuperfluid host, IConstantOutflowNFT constantOutflowNFT - ) ConstantInflowNFT(host, constantOutflowNFT) {} + ) ConstantInflowNFT(host, constantOutflowNFT) { } /// @dev a mock mint function to emit the mint Transfer event function mockMint(address _to, uint256 _newTokenId) public { @@ -64,157 +101,8 @@ contract ConstantInflowNFTMock is ConstantInflowNFT { return _ownerOf(_tokenId); } - /// @dev this exposes the internal flow data by token id for testing purposes - function mockFlowNFTDataByTokenId( - uint256 _tokenId - ) public view returns (FlowNFTData memory flowData) { - return flowDataByTokenId(_tokenId); - } - /// @dev This exposes the _tokenApprovals storage without the requireMinted call function mockGetApproved(uint256 _tokenId) public view returns (address) { return _tokenApprovals[_tokenId]; } -} - -/// @title NoNFTSuperTokenMock -/// @author Superfluid -/// @notice Minimal SuperToken implementation to test flow creation if no NFT proxy contract variable exists. -/// Storage layout is made to mimic SuperToken. -contract NoNFTSuperTokenMock is UUPSProxiable, SuperfluidToken { - using SafeERC20 for IERC20; - - /// @dev The underlying ERC20 token - IERC20 internal _underlyingToken; - - /// @dev Decimals of the underlying token - uint8 internal _underlyingDecimals; - - /// @dev IERC20Metadata Name property - string internal _name; - - /// @dev IERC20Metadata Symbol property - string internal _symbol; - - /// @dev ERC20 Allowances Storage - mapping(address => mapping(address => uint256)) internal _allowances; - - /// @dev ERC777 operators support data - ERC777Helper.Operators internal _operators; - - constructor(ISuperfluid host) SuperfluidToken(host) {} - - /// @dev Initialize the Super Token proxy - function initialize( - IERC20 underlyingToken, - uint8 underlyingDecimals, - string calldata n, - string calldata s - ) - external - initializer // OpenZeppelin Initializable - { - _underlyingToken = underlyingToken; - _underlyingDecimals = underlyingDecimals; - - _name = n; - _symbol = s; - - // register interfaces - ERC777Helper.register(address(this)); - } - - /// @dev ISuperToken.upgrade implementation - function upgrade(uint256 amount) external { - _upgrade(msg.sender, msg.sender, msg.sender, amount, "", ""); - } - - /** - * @dev Handle decimal differences between underlying token and super token - */ - function _toUnderlyingAmount( - uint256 amount - ) private view returns (uint256 underlyingAmount, uint256 adjustedAmount) { - uint256 factor; - if (_underlyingDecimals < 18) { - // if underlying has less decimals - // one can upgrade less "granualar" amount of tokens - factor = 10 ** (18 - _underlyingDecimals); - underlyingAmount = amount / factor; - // remove precision errors - adjustedAmount = underlyingAmount * factor; - } else if (_underlyingDecimals > 18) { - // if underlying has more decimals - // one can upgrade more "granualar" amount of tokens - factor = 10 ** (_underlyingDecimals - 18); - underlyingAmount = amount * factor; - adjustedAmount = amount; - } else { - underlyingAmount = adjustedAmount = amount; - } - } - - function _upgrade( - address operator, - address account, - address to, - uint256 amount, - bytes memory userData, - bytes memory operatorData - ) private { - if (address(_underlyingToken) == address(0)) revert(); - - ( - uint256 underlyingAmount, - uint256 adjustedAmount - ) = _toUnderlyingAmount(amount); - - uint256 amountBefore = _underlyingToken.balanceOf(address(this)); - _underlyingToken.safeTransferFrom( - account, - address(this), - underlyingAmount - ); - uint256 amountAfter = _underlyingToken.balanceOf(address(this)); - uint256 actualUpgradedAmount = amountAfter - amountBefore; - if (underlyingAmount != actualUpgradedAmount) revert(); - - _mint( - operator, - to, - adjustedAmount, - // if `userData.length` than 0, we requireReceptionAck - userData.length != 0, - userData, - operatorData - ); - } - - /// dummy impl - function _mint( - address, // operator, - address account, - uint256 amount, - bool, // requireReceptionAck, - bytes memory, // userData, - bytes memory // operatorData - ) internal { - if (account == address(0)) { - revert(); - } - - SuperfluidToken._mint(account, amount); - } - - function proxiableUUID() public pure override returns (bytes32) { - return - keccak256( - "org.superfluid-finance.contracts.SuperToken.implementation" - ); - } - - // solhint-disable-next-line no-empty-blocks - function updateCode(address newAddress) external override { - // dummy impl - } -} +} \ No newline at end of file diff --git a/packages/ethereum-contracts/contracts/mocks/CFAv1NFTUpgradabilityMock.sol b/packages/ethereum-contracts/contracts/mocks/CFAv1NFTUpgradabilityMock.sol index 593180a7f0..af0ad2d344 100644 --- a/packages/ethereum-contracts/contracts/mocks/CFAv1NFTUpgradabilityMock.sol +++ b/packages/ethereum-contracts/contracts/mocks/CFAv1NFTUpgradabilityMock.sol @@ -5,23 +5,17 @@ import { ISuperfluid } from "../interfaces/superfluid/ISuperfluid.sol"; import { ConstantInflowNFT, IConstantInflowNFT } from "../superfluid/ConstantInflowNFT.sol"; import { ConstantOutflowNFT, IConstantOutflowNFT } from "../superfluid/ConstantOutflowNFT.sol"; import { FlowNFTBase } from "../superfluid/FlowNFTBase.sol"; +import { IStorageLayoutBase } from "./IStorageLayoutBase.sol"; /*////////////////////////////////////////////////////////////////////////// FlowNFTBase Mocks //////////////////////////////////////////////////////////////////////////*/ -interface IFlowNFTBaseMockErrors { - error STORAGE_LOCATION_CHANGED(string _name); -} - /// @title FlowNFTBaseStorageLayoutMock /// @author Superfluid /// @notice A mock FlowNFTBase contract for testing storage layout. /// @dev This contract *MUST* have the same storage layout as FlowNFTBase.sol -contract FlowNFTBaseStorageLayoutMock is FlowNFTBase { - - error STORAGE_LOCATION_CHANGED(string _name); - +contract FlowNFTBaseStorageLayoutMock is FlowNFTBase, IStorageLayoutBase { constructor( ISuperfluid host ) FlowNFTBase(host) {} @@ -96,10 +90,7 @@ contract FlowNFTBaseStorageLayoutMock is FlowNFTBase { /// @author Superfluid /// @notice A mock ConstantOutflowNFT contract for testing storage layout. /// @dev This contract *MUST* have the same storage layout as ConstantOutflowNFT.sol -contract ConstantInflowNFTStorageLayoutMock is ConstantInflowNFT { - - error STORAGE_LOCATION_CHANGED(string _name); - +contract ConstantInflowNFTStorageLayoutMock is ConstantInflowNFT, IStorageLayoutBase { constructor( ISuperfluid host, @@ -136,18 +127,6 @@ contract ConstantInflowNFTStorageLayoutMock is ConstantInflowNFT { } // Dummy implementations for abstract functions - function _ownerOf( - uint256 //tokenId - ) internal pure override returns (address) { - return address(0); - } - function _transfer( - address, //from, - address, //to, - uint256 //tokenId - ) internal pure override { - return; - } function _safeTransfer( address from, address to, @@ -162,10 +141,7 @@ contract ConstantInflowNFTStorageLayoutMock is ConstantInflowNFT { /// @author Superfluid /// @notice A mock ConstantOutflowNFT contract for testing storage layout. /// @dev This contract *MUST* have the same storage layout as ConstantOutflowNFT.sol -contract ConstantOutflowNFTStorageLayoutMock is ConstantOutflowNFT { - - error STORAGE_LOCATION_CHANGED(string _name); - +contract ConstantOutflowNFTStorageLayoutMock is ConstantOutflowNFT, IStorageLayoutBase { constructor( ISuperfluid host, @@ -201,21 +177,10 @@ contract ConstantOutflowNFTStorageLayoutMock is ConstantOutflowNFT { if (slot != 21 || offset != 0) revert STORAGE_LOCATION_CHANGED("_reserve21"); assembly { slot := _flowDataByTokenId.slot offset := _flowDataByTokenId.offset } + if (slot != 22 || offset != 0) revert STORAGE_LOCATION_CHANGED("_flowDataByTokenId"); } // Dummy implementations for abstract functions - function _ownerOf( - uint256 //tokenId - ) internal pure override returns (address) { - return address(0); - } - function _transfer( - address, //from, - address, //to, - uint256 //tokenId - ) internal pure override { - return; - } function _safeTransfer( address from, address to, diff --git a/packages/ethereum-contracts/contracts/mocks/IStorageLayoutBase.sol b/packages/ethereum-contracts/contracts/mocks/IStorageLayoutBase.sol new file mode 100644 index 0000000000..4a1cffbd80 --- /dev/null +++ b/packages/ethereum-contracts/contracts/mocks/IStorageLayoutBase.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +interface IStorageLayoutBase { + error STORAGE_LOCATION_CHANGED(string _name); +} \ No newline at end of file diff --git a/packages/ethereum-contracts/contracts/mocks/PoolNFTMock.sol b/packages/ethereum-contracts/contracts/mocks/PoolNFTMock.sol new file mode 100644 index 0000000000..5e3e642479 --- /dev/null +++ b/packages/ethereum-contracts/contracts/mocks/PoolNFTMock.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: AGPLv3 +// solhint-disable reason-string +pragma solidity 0.8.19; + +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { ISuperfluid } from "../interfaces/superfluid/ISuperfluid.sol"; +import { PoolAdminNFT } from "../agreements/gdav1/PoolAdminNFT.sol"; +import { PoolMemberNFT } from "../agreements/gdav1/PoolMemberNFT.sol"; +import { PoolNFTBase } from "../agreements/gdav1/PoolNFTBase.sol"; + +contract PoolNFTBaseMock is PoolNFTBase { + using Strings for uint256; + + mapping(uint256 => address) private _owners; + + constructor(ISuperfluid host) PoolNFTBase(host) { } + + function proxiableUUID() public pure override returns (bytes32) { + return keccak256("org.superfluid-finance.contracts.PoolNFTBaseMock.implementation"); + } + + /// @dev The owner of here is always the flow sender + function _ownerOf(uint256 tokenId) internal view override returns (address) { + return _owners[tokenId]; + } + + function getTokenId(address pool, address account) external view override returns (uint256 tokenId) { + return _getTokenId(pool, account); + } + + function _getTokenId(address pool, address account) internal view returns (uint256 tokenId) { + return uint256(keccak256(abi.encode("PoolNFTMock", block.chainid, pool, account))); + } + + /// @dev a mock mint function that sets the owner + function mockMint(address pool, address account) public { + uint256 tokenId = _getTokenId(pool, account); + _owners[tokenId] = account; + } + + function _transfer( + address, //from, + address, //to, + uint256 //tokenId + ) internal pure override { + revert POOL_NFT_TRANSFER_NOT_ALLOWED(); + } + + function tokenURI(uint256 tokenId) external pure override returns (string memory) { + return string(abi.encodePacked("tokenId=", tokenId.toString())); + } +} + +contract PoolAdminNFTMock is PoolAdminNFT { + constructor(ISuperfluid host) PoolAdminNFT(host) { } + + /// @dev a mock mint function that exposes the internal _mint function + function mockMint(address _pool) public { + _mint(_pool); + } + + /// @dev this ownerOf doesn't revert if _tokenId doesn't exist + function mockOwnerOf(uint256 _tokenId) public view returns (address) { + return _ownerOf(_tokenId); + } + + /// @dev This exposes the _tokenApprovals storage without the requireMinted call + function mockGetApproved(uint256 _tokenId) public view returns (address) { + return _tokenApprovals[_tokenId]; + } +} + +contract PoolMemberNFTMock is PoolMemberNFT { + constructor(ISuperfluid host) PoolMemberNFT(host) { } + + /// @dev a mock mint function that exposes the internal _mint function + function mockMint(address _pool, address _member) public { + _mint(_pool, _member); + } + + /// @dev a mock burn function that exposes the internal _burn function + function mockBurn(uint256 _tokenId) public { + _burn(_tokenId); + } + + /// @dev this ownerOf doesn't revert if _tokenId doesn't exist + function mockOwnerOf(uint256 _tokenId) public view returns (address) { + return _ownerOf(_tokenId); + } + + /// @dev This exposes the _tokenApprovals storage without the requireMinted call + function mockGetApproved(uint256 _tokenId) public view returns (address) { + return _tokenApprovals[_tokenId]; + } +} diff --git a/packages/ethereum-contracts/contracts/mocks/PoolNFTUpgradabilityMock.sol b/packages/ethereum-contracts/contracts/mocks/PoolNFTUpgradabilityMock.sol new file mode 100644 index 0000000000..2cda01e0f4 --- /dev/null +++ b/packages/ethereum-contracts/contracts/mocks/PoolNFTUpgradabilityMock.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: AGPLv3 +// solhint-disable reason-string +pragma solidity 0.8.19; + +import { PoolNFTBase } from "../agreements/gdav1/PoolNFTBase.sol"; +import { ISuperfluid } from "../interfaces/superfluid/ISuperfluid.sol"; +import { PoolMemberNFT } from "../agreements/gdav1/PoolMemberNFT.sol"; +import { PoolAdminNFT } from "../agreements/gdav1/PoolAdminNFT.sol"; +import { IStorageLayoutBase } from "./IStorageLayoutBase.sol"; + +contract PoolNFTBaseStorageLayoutMock is PoolNFTBase, IStorageLayoutBase { + constructor(ISuperfluid host) PoolNFTBase(host) { } + + function validateStorageLayout() public virtual { + uint256 slot; + uint256 offset; + + assembly { slot := _name.slot offset := _name.offset } + if (slot != 1 || offset != 0) revert STORAGE_LOCATION_CHANGED("_name"); + + assembly { slot := _symbol.slot offset := _symbol.offset } + if (slot != 2 || offset != 0) revert STORAGE_LOCATION_CHANGED("_symbol"); + + assembly { slot := _tokenApprovals.slot offset := _tokenApprovals.offset } + if (slot != 3 || offset != 0) revert STORAGE_LOCATION_CHANGED("_tokenApprovals"); + + assembly { slot := _operatorApprovals.slot offset := _operatorApprovals.offset } + if (slot != 4 || offset != 0) revert STORAGE_LOCATION_CHANGED("_operatorApprovals"); + + assembly { slot := _reserve5.slot offset := _reserve5.offset } + if (slot != 5 || offset != 0) revert STORAGE_LOCATION_CHANGED("_reserve5"); + + assembly { slot := _reserve21.slot offset := _reserve21.offset } + if (slot != 21 || offset != 0) revert STORAGE_LOCATION_CHANGED("_reserve21"); + } + + // Dummy implementations for abstract functions + function _ownerOf( + uint256 //tokenId + ) internal pure override returns (address) { + return address(0); + } + + function getTokenId(address /*pool*/, address /*account*/) external pure override returns (uint256 tokenId) { + return 0; + } + + function _transfer( + address, //from, + address, //to, + uint256 //tokenId + ) internal pure override { + return; + } + + function _safeTransfer( + address from, + address to, + uint256 tokenId, + bytes memory // data + ) internal pure override { + _transfer(from, to, tokenId); + } + + function proxiableUUID() public pure override returns (bytes32) { + return keccak256(""); + } + + function tokenURI(uint256 /*tokenId*/) external pure override returns (string memory) { + return ""; + } +} + +contract PoolAdminNFTStorageLayoutMock is PoolAdminNFT, IStorageLayoutBase { + constructor(ISuperfluid host) PoolAdminNFT(host) { } + + function validateStorageLayout() public virtual { + uint256 slot; + uint256 offset; + + assembly { slot := _name.slot offset := _name.offset } + if (slot != 1 || offset != 0) revert STORAGE_LOCATION_CHANGED("_name"); + + assembly { slot := _symbol.slot offset := _symbol.offset } + if (slot != 2 || offset != 0) revert STORAGE_LOCATION_CHANGED("_symbol"); + + assembly { slot := _tokenApprovals.slot offset := _tokenApprovals.offset } + if (slot != 3 || offset != 0) revert STORAGE_LOCATION_CHANGED("_tokenApprovals"); + + assembly { slot := _operatorApprovals.slot offset := _operatorApprovals.offset } + if (slot != 4 || offset != 0) revert STORAGE_LOCATION_CHANGED("_operatorApprovals"); + + assembly { slot := _reserve5.slot offset := _reserve5.offset } + if (slot != 5 || offset != 0) revert STORAGE_LOCATION_CHANGED("_reserve5"); + + assembly { slot := _reserve21.slot offset := _reserve21.offset } + if (slot != 21 || offset != 0) revert STORAGE_LOCATION_CHANGED("_reserve21"); + + assembly { slot := _poolAdminDataByTokenId.slot offset := _poolAdminDataByTokenId.offset } + if (slot != 22 || offset != 0) revert STORAGE_LOCATION_CHANGED("_poolAdminDataByTokenId"); + } + + // Dummy implementations for abstract functions + function _safeTransfer( + address from, + address to, + uint256 tokenId, + bytes memory // data + ) internal pure override { + _transfer(from, to, tokenId); + } +} + +contract PoolMemberNFTStorageLayoutMock is PoolMemberNFT, IStorageLayoutBase { + constructor(ISuperfluid host) PoolMemberNFT(host) { } + + function validateStorageLayout() public virtual { + uint256 slot; + uint256 offset; + + assembly { slot := _name.slot offset := _name.offset } + if (slot != 1 || offset != 0) revert STORAGE_LOCATION_CHANGED("_name"); + + assembly { slot := _symbol.slot offset := _symbol.offset } + if (slot != 2 || offset != 0) revert STORAGE_LOCATION_CHANGED("_symbol"); + + assembly { slot := _tokenApprovals.slot offset := _tokenApprovals.offset } + if (slot != 3 || offset != 0) revert STORAGE_LOCATION_CHANGED("_tokenApprovals"); + + assembly { slot := _operatorApprovals.slot offset := _operatorApprovals.offset } + if (slot != 4 || offset != 0) revert STORAGE_LOCATION_CHANGED("_operatorApprovals"); + + assembly { slot := _reserve5.slot offset := _reserve5.offset } + if (slot != 5 || offset != 0) revert STORAGE_LOCATION_CHANGED("_reserve5"); + + assembly { slot := _reserve21.slot offset := _reserve21.offset } + if (slot != 21 || offset != 0) revert STORAGE_LOCATION_CHANGED("_reserve21"); + + assembly { slot := _poolMemberDataByTokenId.slot offset := _poolMemberDataByTokenId.offset } + if (slot != 22 || offset != 0) revert STORAGE_LOCATION_CHANGED("_poolMemberDataByTokenId"); + } + + // Dummy implementations for abstract functions + function _safeTransfer( + address from, + address to, + uint256 tokenId, + bytes memory // data + ) internal pure override { + _transfer(from, to, tokenId); + } +} diff --git a/packages/ethereum-contracts/contracts/mocks/SuperTokenFactoryMock.sol b/packages/ethereum-contracts/contracts/mocks/SuperTokenFactoryMock.sol index 6344859281..ae69db9200 100644 --- a/packages/ethereum-contracts/contracts/mocks/SuperTokenFactoryMock.sol +++ b/packages/ethereum-contracts/contracts/mocks/SuperTokenFactoryMock.sol @@ -4,28 +4,25 @@ pragma solidity 0.8.19; import { ISuperfluid, ISuperToken, - SuperTokenFactoryBase, IConstantInflowNFT, - IConstantOutflowNFT -} from "../superfluid/SuperTokenFactory.sol"; + IConstantOutflowNFT, + IPoolAdminNFT, + IPoolMemberNFT +} from "../interfaces/superfluid/ISuperfluid.sol"; +import { SuperTokenFactoryBase } from "../superfluid/SuperTokenFactory.sol"; contract SuperTokenFactoryStorageLayoutTester is SuperTokenFactoryBase { constructor( ISuperfluid host, ISuperToken superTokenLogic, IConstantOutflowNFT constantOutflowNFT, - IConstantInflowNFT constantInflowNFT + IConstantInflowNFT constantInflowNFT, + IPoolAdminNFT poolAdminNFT, + IPoolMemberNFT poolMemberNFT ) - SuperTokenFactoryBase( - host, - superTokenLogic, - constantOutflowNFT, - constantInflowNFT - ) + SuperTokenFactoryBase(host, superTokenLogic, constantOutflowNFT, constantInflowNFT, poolAdminNFT, poolMemberNFT) // solhint-disable-next-line no-empty-blocks - { - - } + { } // @dev Make sure the storage layout never change over the course of the development function validateStorageLayout() external pure { @@ -49,18 +46,13 @@ contract SuperTokenFactoryUpdateLogicContractsTester is SuperTokenFactoryBase { ISuperfluid host, ISuperToken superTokenLogic, IConstantOutflowNFT constantOutflowNFT, - IConstantInflowNFT constantInflowNFT + IConstantInflowNFT constantInflowNFT, + IPoolAdminNFT poolAdminNFT, + IPoolMemberNFT poolMemberNFT ) - SuperTokenFactoryBase( - host, - superTokenLogic, - constantOutflowNFT, - constantInflowNFT - ) + SuperTokenFactoryBase(host, superTokenLogic, constantOutflowNFT, constantInflowNFT, poolAdminNFT, poolMemberNFT) // solhint-disable-next-line no-empty-blocks - { - - } + { } } contract SuperTokenFactoryMock is SuperTokenFactoryBase { @@ -68,18 +60,13 @@ contract SuperTokenFactoryMock is SuperTokenFactoryBase { ISuperfluid host, ISuperToken superTokenLogic, IConstantOutflowNFT constantOutflowNFT, - IConstantInflowNFT constantInflowNFT + IConstantInflowNFT constantInflowNFT, + IPoolAdminNFT poolAdminNFT, + IPoolMemberNFT poolMemberNFT ) - SuperTokenFactoryBase( - host, - superTokenLogic, - constantOutflowNFT, - constantInflowNFT - ) + SuperTokenFactoryBase(host, superTokenLogic, constantOutflowNFT, constantInflowNFT, poolAdminNFT, poolMemberNFT) // solhint-disable-next-line no-empty-blocks - { - - } + { } } contract SuperTokenFactoryMock42 is SuperTokenFactoryBase { @@ -87,16 +74,11 @@ contract SuperTokenFactoryMock42 is SuperTokenFactoryBase { ISuperfluid host, ISuperToken superTokenLogic, IConstantOutflowNFT constantOutflowNFT, - IConstantInflowNFT constantInflowNFT + IConstantInflowNFT constantInflowNFT, + IPoolAdminNFT poolAdminNFT, + IPoolMemberNFT poolMemberNFT ) - SuperTokenFactoryBase( - host, - superTokenLogic, - constantOutflowNFT, - constantInflowNFT - ) + SuperTokenFactoryBase(host, superTokenLogic, constantOutflowNFT, constantInflowNFT, poolAdminNFT, poolMemberNFT) // solhint-disable-next-line no-empty-blocks - { - - } + { } } diff --git a/packages/ethereum-contracts/contracts/mocks/SuperTokenLibraryV1Mock.sol b/packages/ethereum-contracts/contracts/mocks/SuperTokenLibraryV1Mock.sol index dd85539be0..3e5dc35c23 100644 --- a/packages/ethereum-contracts/contracts/mocks/SuperTokenLibraryV1Mock.sol +++ b/packages/ethereum-contracts/contracts/mocks/SuperTokenLibraryV1Mock.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.19; import { ISuperfluid, ISuperToken } from "../interfaces/superfluid/ISuperfluid.sol"; import { SuperAppDefinitions } from "../interfaces/superfluid/ISuperfluid.sol"; +import { ISuperfluidPool } from "../interfaces/agreements/gdav1/ISuperfluidPool.sol"; +import { PoolConfig } from "../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; import { SuperAppBase } from "../apps/SuperAppBase.sol"; import { SuperTokenV1Library } from "../apps/SuperTokenV1Library.sol"; @@ -491,6 +493,74 @@ contract SuperTokenLibraryIDAMock { } } +contract SuperTokenLibraryGDAMock { + using SuperTokenV1Library for ISuperToken; + //// View Functions //// + + function getFlowDistributionFlowRateTest(ISuperToken token, address from, ISuperfluidPool to) + external + view + returns (int96) + { + return token.getFlowDistributionFlowRate(from, to); + } + + function estimateFlowDistributionActualFlowRateTest( + ISuperToken token, + address from, + ISuperfluidPool to, + int96 requestedFlowRate + ) external view returns (int96 actualFlowRate, int96 totalDistributionFlowRate) { + return token.estimateFlowDistributionActualFlowRate(from, to, requestedFlowRate); + } + function estimateDistributionActualAmountTest( + ISuperToken token, + address from, + ISuperfluidPool to, + uint256 requestedAmount + ) external view returns (uint256 actualAmount) { + return token.estimateDistributionActualAmount(from, to, requestedAmount); + } + + function isMemberConnectedTest(ISuperToken token, address pool, address member) + external + view + returns (bool) + { + return token.isMemberConnected(pool, member); + } + + //// Admin/Distributor Operations //// + + function createPoolTest(ISuperToken token, address admin, PoolConfig memory config) + external + { + token.createPool(admin, config); + } + + function distributeToPoolTest(ISuperToken token, address from, ISuperfluidPool pool, uint256 requestedAmount) + external + { + token.distributeToPool(from, pool, requestedAmount); + } + + function distributeFlowTest(ISuperToken token, address from, ISuperfluidPool pool, int96 requestedFlowRate) + external + { + token.distributeFlow(from, pool, requestedFlowRate); + } + + //// Member Operations //// + + function connectPoolTest(ISuperToken token, ISuperfluidPool pool) external { + token.connectPool(pool); + } + + function disconnectPoolTest(ISuperToken token, ISuperfluidPool pool) external { + token.disconnectPool(pool); + } +} + contract SuperTokenLibraryCFASuperAppMock is SuperAppBase { using SuperTokenV1Library for ISuperToken; @@ -499,7 +569,7 @@ contract SuperTokenLibraryCFASuperAppMock is SuperAppBase { address internal immutable sender; address internal immutable receiver; address internal immutable flowOperator; - ISuperfluid internal host; + ISuperfluid internal immutable host; // for selectively testing functions in the same callback enum FunctionIndex { @@ -593,16 +663,13 @@ contract SuperTokenLibraryIDASuperAppMock is SuperTokenLibraryIDAMock, SuperAppB using SuperTokenV1Library for ISuperToken; - bytes internal constant _MOCK_USER_DATA = abi.encode("oh hello"); - ISuperfluid internal host; + ISuperfluid internal immutable host; constructor(ISuperfluid _host) SuperTokenLibraryIDAMock() { host = _host; uint256 configWord = SuperAppDefinitions.APP_LEVEL_FINAL | SuperAppDefinitions.BEFORE_AGREEMENT_CREATED_NOOP | - // SuperAppDefinitions.AFTER_AGREEMENT_CREATED_NOOP | SuperAppDefinitions.BEFORE_AGREEMENT_UPDATED_NOOP | - // SuperAppDefinitions.AFTER_AGREEMENT_UPDATED_NOOP | SuperAppDefinitions.BEFORE_AGREEMENT_TERMINATED_NOOP | SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP; @@ -729,3 +796,84 @@ contract SuperTokenLibraryIDASuperAppMock is SuperTokenLibraryIDAMock, SuperAppB } } } + +// GDA LIBRARY SUPER APP CALLBACK MOCK +contract SuperTokenLibraryGDASuperAppMock is SuperTokenLibraryGDAMock, SuperAppBase { + using SuperTokenV1Library for ISuperToken; + + ISuperfluid internal immutable host; + + constructor(ISuperfluid _host) { + host = _host; + uint256 configWord = SuperAppDefinitions.APP_LEVEL_FINAL | SuperAppDefinitions.BEFORE_AGREEMENT_CREATED_NOOP + | SuperAppDefinitions.BEFORE_AGREEMENT_UPDATED_NOOP | SuperAppDefinitions.BEFORE_AGREEMENT_TERMINATED_NOOP + | SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP; + + host.registerAppWithKey(configWord, ""); + } + + function afterAgreementCreated( + ISuperToken token, + address, + bytes32, + bytes calldata, + bytes calldata, + bytes calldata ctx + ) external override returns (bytes memory newCtx) { + return _callbackTest(token, ctx); + } + + function afterAgreementUpdated( + ISuperToken token, + address, + bytes32, + bytes calldata, + bytes calldata, + bytes calldata ctx + ) external override returns (bytes memory newCtx) { + return _callbackTest(token, ctx); + } + + enum FunctionIndex { + UPDATE_MEMBER_UNITS, + CONNECT_POOL, + DISCONNECT_POOL, + CLAIM_ALL, + DISTRIBUTE, + DISTRIBUTE_FLOW + } + + /// @dev extracts some user data to test out all callback library functions + /// @param token super token + /// @param ctx Context string + /// @return New Context + function _callbackTest(ISuperToken token, bytes memory ctx) internal returns (bytes memory) { + // extract userData, then decode everything else + bytes memory userData = host.decodeCtx(ctx).userData; + ( + uint8 functionIndex, + address pool, + address member, + address from, + uint128 units, + uint256 requestedAmount, + int96 requestedFlowRate + ) = abi.decode(userData, (uint8, address, address, address, uint128, uint256, int96)); + + if (functionIndex == uint8(FunctionIndex.UPDATE_MEMBER_UNITS)) { + return token.updateMemberUnitsWithCtx(ISuperfluidPool(pool), member, units, ctx); + } else if (functionIndex == uint8(FunctionIndex.CONNECT_POOL)) { + return token.connectPoolWithCtx(ISuperfluidPool(pool), ctx); + } else if (functionIndex == uint8(FunctionIndex.DISCONNECT_POOL)) { + return token.disconnectPoolWithCtx(ISuperfluidPool(pool), ctx); + } else if (functionIndex == uint8(FunctionIndex.CLAIM_ALL)) { + return token.claimAllWithCtx(ISuperfluidPool(pool), member, ctx); + } else if (functionIndex == uint8(FunctionIndex.DISTRIBUTE)) { + return token.distributeWithCtx(ISuperfluidPool(pool), from, requestedAmount, ctx); + } else if (functionIndex == uint8(FunctionIndex.DISTRIBUTE_FLOW)) { + return token.distributeFlowWithCtx(from, ISuperfluidPool(pool), requestedFlowRate, ctx); + } else { + revert("invalid function index"); + } + } +} diff --git a/packages/ethereum-contracts/contracts/mocks/SuperTokenMock.sol b/packages/ethereum-contracts/contracts/mocks/SuperTokenMock.sol index f70d5c4b95..7e336c249f 100644 --- a/packages/ethereum-contracts/contracts/mocks/SuperTokenMock.sol +++ b/packages/ethereum-contracts/contracts/mocks/SuperTokenMock.sol @@ -1,18 +1,25 @@ // SPDX-License-Identifier: AGPLv3 pragma solidity 0.8.19; -import { ISuperfluid, IConstantInflowNFT, IConstantOutflowNFT } from "../interfaces/superfluid/ISuperfluid.sol"; +import { + ISuperfluid, IERC20, IConstantInflowNFT, IConstantOutflowNFT, IPoolAdminNFT, IPoolMemberNFT +} from "../interfaces/superfluid/ISuperfluid.sol"; +import { UUPSProxiable } from "../upgradability/UUPSProxiable.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { ERC777Helper } from "../libs/ERC777Helper.sol"; import { SuperToken } from "../superfluid/SuperToken.sol"; +import { SuperfluidToken } from "../superfluid/SuperfluidToken.sol"; contract SuperTokenStorageLayoutTester is SuperToken { - constructor( ISuperfluid host, IConstantOutflowNFT constantOutflowNFTProxy, - IConstantInflowNFT constantInflowNFTProxy - ) - SuperToken(host, constantOutflowNFTProxy, constantInflowNFTProxy) // solhint-disable-next-line no-empty-blocks - {} + IConstantInflowNFT constantInflowNFTProxy, + IPoolAdminNFT poolAdminNFTProxy, + IPoolMemberNFT poolMemberNFTProxy + ) SuperToken(host, constantOutflowNFTProxy, constantInflowNFTProxy, poolAdminNFTProxy, poolMemberNFTProxy) + // solhint-disable-next-line no-empty-blocks + { } // @dev Make sure the storage layout never change over the course of the development function validateStorageLayout() external pure { @@ -72,15 +79,16 @@ contract SuperTokenStorageLayoutTester is SuperToken { } contract SuperTokenMock is SuperToken { - - uint256 immutable public waterMark; + uint256 public immutable waterMark; constructor( ISuperfluid host, uint256 w, IConstantOutflowNFT constantOutflowNFTProxy, - IConstantInflowNFT constantInflowNFTProxy - ) SuperToken(host, constantOutflowNFTProxy, constantInflowNFTProxy) { + IConstantInflowNFT constantInflowNFTProxy, + IPoolAdminNFT poolAdminNFTProxy, + IPoolMemberNFT poolMemberNFTProxy + ) SuperToken(host, constantOutflowNFTProxy, constantInflowNFTProxy, poolAdminNFTProxy, poolMemberNFTProxy) { waterMark = w; } @@ -102,12 +110,134 @@ contract SuperTokenMock is SuperToken { _setupDefaultOperators(operators); } - function mintInternal( + function mintInternal(address to, uint256 amount, bytes memory userData, bytes memory operatorData) external { + _mint(msg.sender, to, amount, true, /* invokeHook */ true, /* requireReceptionAck */ userData, operatorData); + } +} + +/// @title NoNFTSuperTokenMock +/// @author Superfluid +/// @notice Minimal SuperToken implementation to test flow creation if no NFT proxy contract variable exists. +/// Storage layout is made to mimic SuperToken. +contract NoNFTSuperTokenMock is UUPSProxiable, SuperfluidToken { + using SafeERC20 for IERC20; + + /// @dev The underlying ERC20 token + IERC20 internal _underlyingToken; + + /// @dev Decimals of the underlying token + uint8 internal _underlyingDecimals; + + /// @dev TokenInfo Name property + string internal _name; + + /// @dev TokenInfo Symbol property + string internal _symbol; + + /// @dev ERC20 Allowances Storage + mapping(address => mapping(address => uint256)) internal _allowances; + + /// @dev ERC777 operators support data + ERC777Helper.Operators internal _operators; + + constructor(ISuperfluid host) SuperfluidToken(host) { } + + /// @dev Initialize the Super Token proxy + function initialize(IERC20 underlyingToken, uint8 underlyingDecimals, string calldata n, string calldata s) + external + initializer // OpenZeppelin Initializable + { + _underlyingToken = underlyingToken; + _underlyingDecimals = underlyingDecimals; + + _name = n; + _symbol = s; + + // register interfaces + ERC777Helper.register(address(this)); + } + + /// @dev ISuperToken.upgrade implementation + function upgrade(uint256 amount) external { + _upgrade(msg.sender, msg.sender, msg.sender, amount, "", ""); + } + + /** + * @dev Handle decimal differences between underlying token and super token + */ + function _toUnderlyingAmount(uint256 amount) + private + view + returns (uint256 underlyingAmount, uint256 adjustedAmount) + { + uint256 factor; + if (_underlyingDecimals < 18) { + // if underlying has less decimals + // one can upgrade less "granualar" amount of tokens + factor = 10 ** (18 - _underlyingDecimals); + underlyingAmount = amount / factor; + // remove precision errors + adjustedAmount = underlyingAmount * factor; + } else if (_underlyingDecimals > 18) { + // if underlying has more decimals + // one can upgrade more "granualar" amount of tokens + factor = 10 ** (_underlyingDecimals - 18); + underlyingAmount = amount * factor; + adjustedAmount = amount; + } else { + underlyingAmount = adjustedAmount = amount; + } + } + + function _upgrade( + address operator, + address account, address to, uint256 amount, bytes memory userData, bytes memory operatorData - ) external { - _mint(msg.sender, to, amount, true /* invokeHook */, true /* requireReceptionAck */, userData, operatorData); + ) private { + if (address(_underlyingToken) == address(0)) revert(""); + + (uint256 underlyingAmount, uint256 adjustedAmount) = _toUnderlyingAmount(amount); + + uint256 amountBefore = _underlyingToken.balanceOf(address(this)); + _underlyingToken.safeTransferFrom(account, address(this), underlyingAmount); + uint256 amountAfter = _underlyingToken.balanceOf(address(this)); + uint256 actualUpgradedAmount = amountAfter - amountBefore; + if (underlyingAmount != actualUpgradedAmount) revert(""); + + _mint( + operator, + to, + adjustedAmount, + // if `userData.length` than 0, we requireReceptionAck + userData.length != 0, + userData, + operatorData + ); + } + + /// dummy impl + function _mint( + address, // operator, + address account, + uint256 amount, + bool, // requireReceptionAck, + bytes memory, // userData, + bytes memory // operatorData + ) internal { + if (account == address(0)) { + revert(""); + } + + SuperfluidToken._mint(account, amount); } + + function proxiableUUID() public pure override returns (bytes32) { + return keccak256("org.superfluid-finance.contracts.SuperToken.implementation"); + } + + // solhint-disable-next-line no-empty-blocks + function updateCode(address newAddress) external override { } } diff --git a/packages/ethereum-contracts/contracts/mocks/SuperfluidPoolUpgradabilityMock.sol b/packages/ethereum-contracts/contracts/mocks/SuperfluidPoolUpgradabilityMock.sol new file mode 100644 index 0000000000..8af9ec6002 --- /dev/null +++ b/packages/ethereum-contracts/contracts/mocks/SuperfluidPoolUpgradabilityMock.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import { GeneralDistributionAgreementV1 } from "../agreements/gdav1/GeneralDistributionAgreementV1.sol"; +import { SuperfluidPool } from "../agreements/gdav1/SuperfluidPool.sol"; +import { IStorageLayoutBase } from "./IStorageLayoutBase.sol"; + +/// @title SuperfluidPoolStorageLayoutMock +/// @notice A mock SuperfluidPool contract for testing storage layout. +/// @dev This contract *MUST* have the same storage layout as SuperfluidPool. +contract SuperfluidPoolStorageLayoutMock is SuperfluidPool, IStorageLayoutBase { + constructor(GeneralDistributionAgreementV1 gda_) SuperfluidPool(gda_) { } + + function validateStorageLayout() public pure { + uint256 slot; + uint256 offset; + + // offset of 2 is taken by the following variables: + // Initializable._initialized (uint8) 1byte + // Initializable._initializing (bool) 1byte + + assembly { slot := superToken.slot offset := superToken.offset } + if (slot != 0 || offset != 2) revert STORAGE_LOCATION_CHANGED("superToken"); + + assembly { slot := admin.slot offset := admin.offset } + if (slot != 1 || offset != 0) revert STORAGE_LOCATION_CHANGED("admin"); + + assembly { slot := _index.slot offset := _index.offset } + if (slot != 2 || offset != 0) revert STORAGE_LOCATION_CHANGED("_index"); + // slot 2: uint128 total units | uint32 wrappedSettledAt | int96 wrappedFlowRate + // slot 3: int256 wrappedSettledValue + + assembly { slot := _membersData.slot offset := _membersData.offset } + if (slot != 4 || offset != 0) revert STORAGE_LOCATION_CHANGED("_membersData"); + + assembly { slot := _disconnectedMembers.slot offset := _disconnectedMembers.offset } + if (slot != 5 || offset != 0) revert STORAGE_LOCATION_CHANGED("_disconnectedMembers"); + // slot 5: uint128 ownedUnits | uint32 syncedSettledAt | int96 syncedFlowRate + // slot 6: int256 syncedSettledValue + // slot 7: int256 settledValue + // slot 8: int256 claimedValue + + assembly { slot := _allowances.slot offset := _allowances.offset } + if (slot != 9 || offset != 0) revert STORAGE_LOCATION_CHANGED("_allowances"); + + assembly { slot := transferabilityForUnitsOwner.slot offset := transferabilityForUnitsOwner.offset } + if (slot != 10 || offset != 0) revert STORAGE_LOCATION_CHANGED("transferabilityForUnitsOwner"); + + assembly { slot := distributionFromAnyAddress.slot offset := distributionFromAnyAddress.offset } + if (slot != 10 || offset != 1) revert STORAGE_LOCATION_CHANGED("distributionFromAnyAddress"); + } +} diff --git a/packages/ethereum-contracts/contracts/superfluid/ConstantInflowNFT.sol b/packages/ethereum-contracts/contracts/superfluid/ConstantInflowNFT.sol index b8ff39e181..5603aa8fca 100644 --- a/packages/ethereum-contracts/contracts/superfluid/ConstantInflowNFT.sol +++ b/packages/ethereum-contracts/contracts/superfluid/ConstantInflowNFT.sol @@ -15,18 +15,12 @@ contract ConstantInflowNFT is FlowNFTBase, IConstantInflowNFT { IConstantOutflowNFT public immutable CONSTANT_OUTFLOW_NFT; // solhint-disable-next-line no-empty-blocks - constructor( - ISuperfluid host, - IConstantOutflowNFT constantOutflowNFT - ) FlowNFTBase(host) { + constructor(ISuperfluid host, IConstantOutflowNFT constantOutflowNFT) FlowNFTBase(host) { CONSTANT_OUTFLOW_NFT = constantOutflowNFT; } function proxiableUUID() public pure override returns (bytes32) { - return - keccak256( - "org.superfluid-finance.contracts.ConstantInflowNFT.implementation" - ); + return keccak256("org.superfluid-finance.contracts.ConstantInflowNFT.implementation"); } /// @notice The mint function emits the "mint" `Transfer` event. @@ -35,10 +29,7 @@ contract ConstantInflowNFT is FlowNFTBase, IConstantInflowNFT { /// Only callable by ConstantOutflowNFT /// @param to the receiver of the inflow nft and desired flow receiver /// @param newTokenId the new token id - function mint( - address to, - uint256 newTokenId - ) external onlyConstantOutflowNFT { + function mint(address to, uint256 newTokenId) external onlyConstantOutflowNFT { _mint(to, newTokenId); } @@ -51,9 +42,7 @@ contract ConstantInflowNFT is FlowNFTBase, IConstantInflowNFT { _burn(tokenId); } - function flowDataByTokenId( - uint256 tokenId - ) + function flowDataByTokenId(uint256 tokenId) public view override(FlowNFTBase, IFlowNFTBase) @@ -62,21 +51,12 @@ contract ConstantInflowNFT is FlowNFTBase, IConstantInflowNFT { flowData = CONSTANT_OUTFLOW_NFT.flowDataByTokenId(tokenId); } - function tokenURI( - uint256 tokenId - ) - external - view - override(FlowNFTBase, IERC721Metadata) - returns (string memory) - { + function tokenURI(uint256 tokenId) external view override(FlowNFTBase, IERC721Metadata) returns (string memory) { return _tokenURI(tokenId, true); } /// @inheritdoc FlowNFTBase - function _ownerOf( - uint256 tokenId - ) internal view virtual override returns (address) { + function _ownerOf(uint256 tokenId) internal view override returns (address) { FlowNFTData memory flowData = flowDataByTokenId(tokenId); return flowData.flowReceiver; } @@ -87,7 +67,7 @@ contract ConstantInflowNFT is FlowNFTBase, IConstantInflowNFT { address, // from, address, // to, uint256 // tokenId - ) internal virtual override { + ) internal pure override { revert CFA_NFT_TRANSFER_IS_NOT_ALLOWED(); } diff --git a/packages/ethereum-contracts/contracts/superfluid/ConstantOutflowNFT.sol b/packages/ethereum-contracts/contracts/superfluid/ConstantOutflowNFT.sol index 01409d3d3d..63e311d42d 100644 --- a/packages/ethereum-contracts/contracts/superfluid/ConstantOutflowNFT.sol +++ b/packages/ethereum-contracts/contracts/superfluid/ConstantOutflowNFT.sol @@ -22,27 +22,19 @@ contract ConstantOutflowNFT is FlowNFTBase, IConstantOutflowNFT { mapping(uint256 => FlowNFTData) internal _flowDataByTokenId; // solhint-disable-next-line no-empty-blocks - constructor( - ISuperfluid host, - IConstantInflowNFT constantInflowNFT - ) FlowNFTBase(host) { + constructor(ISuperfluid host, IConstantInflowNFT constantInflowNFT) FlowNFTBase(host) { CONSTANT_INFLOW_NFT = constantInflowNFT; } // note that this is used so we don't upgrade to wrong logic contract function proxiableUUID() public pure override returns (bytes32) { - return - keccak256( - "org.superfluid-finance.contracts.ConstantOutflowNFT.implementation" - ); + return keccak256("org.superfluid-finance.contracts.ConstantOutflowNFT.implementation"); } /// @notice An external function for querying flow data by `tokenId`` /// @param tokenId the token id /// @return flowData the flow data associated with `tokenId` - function flowDataByTokenId( - uint256 tokenId - ) + function flowDataByTokenId(uint256 tokenId) public view override(FlowNFTBase, IFlowNFTBase) @@ -51,14 +43,7 @@ contract ConstantOutflowNFT is FlowNFTBase, IConstantOutflowNFT { flowData = _flowDataByTokenId[tokenId]; } - function tokenURI( - uint256 tokenId - ) - external - view - override(FlowNFTBase, IERC721Metadata) - returns (string memory) - { + function tokenURI(uint256 tokenId) external view override(FlowNFTBase, IERC721Metadata) returns (string memory) { return _tokenURI(tokenId, false); } @@ -68,18 +53,13 @@ contract ConstantOutflowNFT is FlowNFTBase, IConstantOutflowNFT { /// @param flowSender the flow sender /// @param flowReceiver the flow receiver /// NOTE: We do an existence check in here to determine whether or not to execute the hook - function onCreate( - ISuperfluidToken superToken, - address flowSender, - address flowReceiver - ) external onlyFlowAgreements { + function onCreate(ISuperfluidToken superToken, address flowSender, address flowReceiver) + external + onlyFlowAgreements + { // we don't check matching super token because the nft token id // is generated based on the superToken - uint256 newTokenId = _getTokenId( - address(superToken), - flowSender, - flowReceiver - ); + uint256 newTokenId = _getTokenId(address(superToken), flowSender, flowReceiver); if (_flowDataByTokenId[newTokenId].flowSender == address(0)) { _mint(address(superToken), flowSender, flowReceiver, newTokenId); @@ -93,16 +73,11 @@ contract ConstantOutflowNFT is FlowNFTBase, IConstantOutflowNFT { /// @param flowSender the flow sender /// @param flowReceiver the flow receiver /// NOTE: We do an existence check in here to determine whether or not to execute the hook - function onUpdate( - ISuperfluidToken superToken, - address flowSender, - address flowReceiver - ) external onlyFlowAgreements { - uint256 tokenId = _getTokenId( - address(superToken), - flowSender, - flowReceiver - ); + function onUpdate(ISuperfluidToken superToken, address flowSender, address flowReceiver) + external + onlyFlowAgreements + { + uint256 tokenId = _getTokenId(address(superToken), flowSender, flowReceiver); if (_flowDataByTokenId[tokenId].flowSender != address(0)) { _triggerMetadataUpdate(tokenId); @@ -116,16 +91,11 @@ contract ConstantOutflowNFT is FlowNFTBase, IConstantOutflowNFT { /// @param flowSender the flow sender /// @param flowReceiver the flow receiver /// NOTE: We do an existence check in here to determine whether or not to execute the hook - function onDelete( - ISuperfluidToken superToken, - address flowSender, - address flowReceiver - ) external onlyFlowAgreements { - uint256 tokenId = _getTokenId( - address(superToken), - flowSender, - flowReceiver - ); + function onDelete(ISuperfluidToken superToken, address flowSender, address flowReceiver) + external + onlyFlowAgreements + { + uint256 tokenId = _getTokenId(address(superToken), flowSender, flowReceiver); if (_flowDataByTokenId[tokenId].flowSender != address(0)) { // must "burn" inflow NFT first because we clear storage when burning outflow NFT @@ -136,9 +106,7 @@ contract ConstantOutflowNFT is FlowNFTBase, IConstantOutflowNFT { } /// @inheritdoc FlowNFTBase - function _ownerOf( - uint256 tokenId - ) internal view virtual override returns (address) { + function _ownerOf(uint256 tokenId) internal view override returns (address) { return _flowDataByTokenId[tokenId].flowSender; } @@ -148,7 +116,7 @@ contract ConstantOutflowNFT is FlowNFTBase, IConstantOutflowNFT { address, // from, address, // to, uint256 // tokenId - ) internal virtual override { + ) internal pure override { revert CFA_NFT_TRANSFER_IS_NOT_ALLOWED(); } @@ -159,12 +127,7 @@ contract ConstantOutflowNFT is FlowNFTBase, IConstantOutflowNFT { /// @param flowSender the receiver of the newly minted outflow nft (to) /// @param flowReceiver the flow receiver (owner of the InflowNFT) /// @param newTokenId the new token id to be minted - function _mint( - address superToken, - address flowSender, - address flowReceiver, - uint256 newTokenId - ) internal { + function _mint(address superToken, address flowSender, address flowReceiver, uint256 newTokenId) internal { assert(flowSender != address(0)); assert(flowSender != flowReceiver); assert(!_exists(newTokenId)); @@ -197,7 +160,10 @@ contract ConstantOutflowNFT is FlowNFTBase, IConstantOutflowNFT { } modifier onlyFlowAgreements() { - if (msg.sender != address(CONSTANT_FLOW_AGREEMENT_V1)) { + if ( + msg.sender != address(CONSTANT_FLOW_AGREEMENT_V1) + && msg.sender != address(GENERAL_DISTRIBUTION_AGREEMENT_V1) + ) { revert COF_NFT_ONLY_FLOW_AGREEMENTS(); } _; diff --git a/packages/ethereum-contracts/contracts/superfluid/FlowNFTBase.sol b/packages/ethereum-contracts/contracts/superfluid/FlowNFTBase.sol index 58a3c8faee..a67ca5c55c 100644 --- a/packages/ethereum-contracts/contracts/superfluid/FlowNFTBase.sol +++ b/packages/ethereum-contracts/contracts/superfluid/FlowNFTBase.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: AGPLv3 pragma solidity 0.8.19; -// We use reserved slots for upgradable contracts. // solhint-disable max-states-count +// Notes: We use reserved slots for upgradable contracts. // They are used in solidity docs. import { @@ -13,7 +13,8 @@ import { import { UUPSProxiable } from "../upgradability/UUPSProxiable.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { - ISuperfluid, ISuperToken, ISuperTokenFactory, IFlowNFTBase, IConstantFlowAgreementV1 + ISuperfluid, ISuperToken, ISuperTokenFactory, IFlowNFTBase, + IConstantFlowAgreementV1, IGeneralDistributionAgreementV1 } from "../interfaces/superfluid/ISuperfluid.sol"; /// @title FlowNFTBase abstract contract @@ -37,6 +38,12 @@ abstract contract FlowNFTBase is UUPSProxiable, IFlowNFTBase { // solhint-disable-next-line var-name-mixedcase IConstantFlowAgreementV1 public immutable CONSTANT_FLOW_AGREEMENT_V1; + /// @notice GeneralDistributionAgreementV1 contract address + /// @dev This is the address of the GDAv1 contract cached so we don't have to + /// do an external call for every flow created. + // solhint-disable-next-line var-name-mixedcase + IGeneralDistributionAgreementV1 public immutable GENERAL_DISTRIBUTION_AGREEMENT_V1; + /// @notice Superfluid host contract address ISuperfluid public immutable HOST; @@ -66,7 +73,7 @@ abstract contract FlowNFTBase is UUPSProxiable, IFlowNFTBase { /// without having to worry about messing up the storage layout that exists in COFNFT or CIFNFT. /// @dev This empty reserved space is put in place to allow future versions to add new /// variables without shifting down storage in the inheritance chain. - /// Slots 6-21 are reserved for future use. + /// Slots 5-21 are reserved for future use. /// We use this pattern in SuperToken.sol and favor this over the OpenZeppelin pattern /// as this prevents silly footgunning. /// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps @@ -97,12 +104,16 @@ abstract contract FlowNFTBase is UUPSProxiable, IFlowNFTBase { ) ) ); + GENERAL_DISTRIBUTION_AGREEMENT_V1 = IGeneralDistributionAgreementV1( + address( + ISuperfluid(host).getAgreementClass( + keccak256("org.superfluid-finance.agreements.GeneralDistributionAgreement.v1") + ) + ) + ); } - function initialize( - string memory nftName, - string memory nftSymbol - ) + function initialize(string memory nftName, string memory nftSymbol) external override initializer // OpenZeppelin Initializable @@ -132,19 +143,14 @@ abstract contract FlowNFTBase is UUPSProxiable, IFlowNFTBase { /// @param interfaceId the XOR of all function selectors in the interface /// @return boolean true if the interface is supported /// @inheritdoc IERC165 - function supportsInterface( - bytes4 interfaceId - ) external pure virtual override returns (bool) { - return - interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 - interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 - interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata + function supportsInterface(bytes4 interfaceId) external pure virtual override returns (bool) { + return interfaceId == 0x01ffc9a7 // ERC165 Interface ID for ERC165 + || interfaceId == 0x80ac58cd // ERC165 Interface ID for ERC721 + || interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata } /// @inheritdoc IERC721 - function ownerOf( - uint256 tokenId - ) public view virtual override returns (address) { + function ownerOf(uint256 tokenId) public view virtual override returns (address) { address owner = _ownerOf(tokenId); if (owner == address(0)) { revert CFA_NFT_INVALID_TOKEN_ID(); @@ -178,23 +184,14 @@ abstract contract FlowNFTBase is UUPSProxiable, IFlowNFTBase { /// @notice This returns the Uniform Resource Identifier (URI), where the metadata for the NFT lives. /// @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. /// @return the token URI - function tokenURI( - uint256 tokenId - ) external view virtual returns (string memory); + function tokenURI(uint256 tokenId) external view virtual returns (string memory); - function _tokenURI( - uint256 tokenId, - bool isInflow - ) internal view virtual returns (string memory) { + function _tokenURI(uint256 tokenId, bool isInflow) internal view virtual returns (string memory) { FlowNFTData memory flowData = flowDataByTokenId(tokenId); ISuperToken token = ISuperToken(flowData.superToken); - (, int96 flowRate, , ) = CONSTANT_FLOW_AGREEMENT_V1.getFlow( - token, - flowData.flowSender, - flowData.flowReceiver - ); + (, int96 flowRate,,) = CONSTANT_FLOW_AGREEMENT_V1.getFlow(token, flowData.flowSender, flowData.flowReceiver); return string( @@ -209,44 +206,31 @@ abstract contract FlowNFTBase is UUPSProxiable, IFlowNFTBase { ); } - function _flowDataString( - uint256 tokenId - ) internal view returns (string memory) { + function _flowDataString(uint256 tokenId) internal view returns (string memory) { FlowNFTData memory flowData = flowDataByTokenId(tokenId); // @note taking this out to deal with the stack too deep issue // which occurs when you are attempting to abi.encodePacked // too many elements - return - string( - abi.encodePacked( - "&token_address=", - Strings.toHexString( - uint256(uint160(flowData.superToken)), - 20 - ), - "&chain_id=", - block.chainid.toString(), - "&token_symbol=", - ISuperToken(flowData.superToken).symbol(), - "&sender=", - Strings.toHexString( - uint256(uint160(flowData.flowSender)), - 20 - ), - "&receiver=", - Strings.toHexString( - uint256(uint160(flowData.flowReceiver)), - 20 - ), - "&token_decimals=", - uint256(ISuperToken(flowData.superToken).decimals()) - .toString(), - "&start_date=", - // @note upcasting is safe - uint256(flowData.flowStartDate).toString() - ) - ); + return string( + abi.encodePacked( + "&token_address=", + Strings.toHexString(uint256(uint160(flowData.superToken)), 20), + "&chain_id=", + block.chainid.toString(), + "&token_symbol=", + ISuperToken(flowData.superToken).symbol(), + "&sender=", + Strings.toHexString(uint256(uint160(flowData.flowSender)), 20), + "&receiver=", + Strings.toHexString(uint256(uint160(flowData.flowReceiver)), 20), + "&token_decimals=", + uint256(ISuperToken(flowData.superToken).decimals()).toString(), + "&start_date=", + // @note upcasting is safe + uint256(flowData.flowStartDate).toString() + ) + ); } /// @inheritdoc IERC721 @@ -264,55 +248,37 @@ abstract contract FlowNFTBase is UUPSProxiable, IFlowNFTBase { } /// @inheritdoc IFlowNFTBase - function getTokenId( - address superToken, - address sender, - address receiver - ) external view returns (uint256 tokenId) { + function getTokenId(address superToken, address sender, address receiver) external view returns (uint256 tokenId) { tokenId = _getTokenId(superToken, sender, receiver); } - function _getTokenId( - address superToken, - address sender, - address receiver - ) internal view returns (uint256 tokenId) { - tokenId = uint256( - keccak256(abi.encode(block.chainid, superToken, sender, receiver)) - ); + function _getTokenId(address superToken, address sender, address receiver) + internal + view + returns (uint256 tokenId) + { + tokenId = uint256(keccak256(abi.encode(block.chainid, superToken, sender, receiver))); } /// @inheritdoc IERC721 - function getApproved( - uint256 tokenId - ) public view virtual override returns (address) { + function getApproved(uint256 tokenId) public view virtual override returns (address) { _requireMinted(tokenId); return _tokenApprovals[tokenId]; } /// @inheritdoc IERC721 - function setApprovalForAll( - address operator, - bool approved - ) external virtual override { + function setApprovalForAll(address operator, bool approved) external virtual override { _setApprovalForAll(msg.sender, operator, approved); } /// @inheritdoc IERC721 - function isApprovedForAll( - address owner, - address operator - ) public view virtual override returns (bool) { + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { return _operatorApprovals[owner][operator]; } /// @inheritdoc IERC721 - function transferFrom( - address from, - address to, - uint256 tokenId - ) external virtual override { + function transferFrom(address from, address to, uint256 tokenId) external virtual override { if (!_isApprovedOrOwner(msg.sender, tokenId)) { revert CFA_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL(); } @@ -321,21 +287,12 @@ abstract contract FlowNFTBase is UUPSProxiable, IFlowNFTBase { } /// @inheritdoc IERC721 - function safeTransferFrom( - address from, - address to, - uint256 tokenId - ) external virtual override { + function safeTransferFrom(address from, address to, uint256 tokenId) external virtual override { safeTransferFrom(from, to, tokenId, ""); } /// @inheritdoc IERC721 - function safeTransferFrom( - address from, - address to, - uint256 tokenId, - bytes memory data - ) public virtual override { + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override { if (!_isApprovedOrOwner(msg.sender, tokenId)) { revert CFA_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL(); } @@ -348,14 +305,9 @@ abstract contract FlowNFTBase is UUPSProxiable, IFlowNFTBase { /// @param spender the spender of the token /// @param tokenId the id of the token to be spent /// @return whether `tokenId` can be spent by `spender` - function _isApprovedOrOwner( - address spender, - uint256 tokenId - ) internal view returns (bool) { + function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) { address owner = FlowNFTBase.ownerOf(tokenId); - return (spender == owner || - isApprovedForAll(owner, spender) || - getApproved(tokenId) == spender); + return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender); } /// @notice Reverts if `tokenId` doesn't exist @@ -365,8 +317,7 @@ abstract contract FlowNFTBase is UUPSProxiable, IFlowNFTBase { } /// @notice Returns whether `tokenId` exists - /// @dev Explain to a developer any extra details - /// Tokens can be managed by their owner or approved accounts via `approve` or `setApprovalForAll`. + /// @dev Tokens can be managed by their owner or approved accounts via `approve` or `setApprovalForAll`. /// Tokens start existing when they are minted (`_mint`), /// and stop existing when they are burned (`_burn`). /// @param tokenId the token id we're interested in seeing if exists @@ -385,11 +336,7 @@ abstract contract FlowNFTBase is UUPSProxiable, IFlowNFTBase { emit Approval(_ownerOf(tokenId), to, tokenId); } - function _setApprovalForAll( - address owner, - address operator, - bool approved - ) internal { + function _setApprovalForAll(address owner, address operator, bool approved) internal { if (owner == operator) revert CFA_NFT_APPROVE_TO_CALLER(); _operatorApprovals[owner][operator] = approved; @@ -400,20 +347,14 @@ abstract contract FlowNFTBase is UUPSProxiable, IFlowNFTBase { /// @dev Returns the flow data of the `tokenId`. Does NOT revert if token doesn't exist. /// @param tokenId the token id whose existence we're checking /// @return flowData the FlowNFTData struct for `tokenId` - function flowDataByTokenId( - uint256 tokenId - ) public view virtual returns (FlowNFTData memory flowData); + function flowDataByTokenId(uint256 tokenId) public view virtual returns (FlowNFTData memory flowData); /// @dev Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist. /// @param tokenId the token id whose existence we're checking /// @return address the address of the owner of `tokenId` function _ownerOf(uint256 tokenId) internal view virtual returns (address); - function _transfer( - address from, - address to, - uint256 tokenId - ) internal virtual; + function _transfer(address from, address to, uint256 tokenId) internal virtual; function _safeTransfer( address from, diff --git a/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol b/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol index 00a4e3f950..2e253d62a9 100644 --- a/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol +++ b/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol @@ -10,7 +10,9 @@ import { ISuperToken, IERC20, IConstantOutflowNFT, - IConstantInflowNFT + IConstantInflowNFT, + IPoolAdminNFT, + IPoolMemberNFT } from "../interfaces/superfluid/ISuperfluid.sol"; import { SuperfluidToken } from "./SuperfluidToken.sol"; import { ERC777Helper } from "../libs/ERC777Helper.sol"; @@ -49,6 +51,12 @@ contract SuperToken is // solhint-disable-next-line var-name-mixedcase IConstantInflowNFT immutable public CONSTANT_INFLOW_NFT; + // solhint-disable-next-line var-name-mixedcase + IPoolMemberNFT immutable public POOL_MEMBER_NFT; + + // solhint-disable-next-line var-name-mixedcase + IPoolAdminNFT immutable public POOL_ADMIN_NFT; + /* WARNING: NEVER RE-ORDER VARIABLES! Including the base contracts. Always double-check that new variables are added APPEND-ONLY. Re-ordering variables can @@ -96,7 +104,9 @@ contract SuperToken is constructor( ISuperfluid host, IConstantOutflowNFT constantOutflowNFT, - IConstantInflowNFT constantInflowNFT + IConstantInflowNFT constantInflowNFT, + IPoolAdminNFT poolAdminNFT, + IPoolMemberNFT poolMemberNFT ) SuperfluidToken(host) // solhint-disable-next-line no-empty-blocks @@ -107,9 +117,14 @@ contract SuperToken is // set the immutable canonical NFT proxy addresses CONSTANT_OUTFLOW_NFT = constantOutflowNFT; CONSTANT_INFLOW_NFT = constantInflowNFT; + POOL_ADMIN_NFT = poolAdminNFT; + POOL_MEMBER_NFT = poolMemberNFT; emit ConstantOutflowNFTCreated(constantOutflowNFT); emit ConstantInflowNFTCreated(constantInflowNFT); + + emit PoolAdminNFTCreated(poolAdminNFT); + emit PoolMemberNFTCreated(poolMemberNFT); } /// @dev Initialize the Super Token proxy diff --git a/packages/ethereum-contracts/contracts/superfluid/SuperTokenFactory.sol b/packages/ethereum-contracts/contracts/superfluid/SuperTokenFactory.sol index 9d4c09464e..7030064d4d 100644 --- a/packages/ethereum-contracts/contracts/superfluid/SuperTokenFactory.sol +++ b/packages/ethereum-contracts/contracts/superfluid/SuperTokenFactory.sol @@ -7,7 +7,9 @@ import { ISuperTokenFactory, ISuperToken } from "../interfaces/superfluid/ISuperTokenFactory.sol"; -import { ISuperfluid, IConstantOutflowNFT, IConstantInflowNFT } from "../interfaces/superfluid/ISuperfluid.sol"; +import { + ISuperfluid, IConstantOutflowNFT, IConstantInflowNFT, IPoolAdminNFT, IPoolMemberNFT +} from "../interfaces/superfluid/ISuperfluid.sol"; import { UUPSProxy } from "../upgradability/UUPSProxy.sol"; import { UUPSProxiable } from "../upgradability/UUPSProxiable.sol"; import { FullUpgradableSuperTokenProxy } from "./FullUpgradableSuperTokenProxy.sol"; @@ -36,6 +38,12 @@ abstract contract SuperTokenFactoryBase is // solhint-disable-next-line var-name-mixedcase IConstantInflowNFT immutable public CONSTANT_INFLOW_NFT_LOGIC; + // solhint-disable-next-line var-name-mixedcase + IPoolAdminNFT immutable public POOL_ADMIN_NFT_LOGIC; + + // solhint-disable-next-line var-name-mixedcase + IPoolMemberNFT immutable public POOL_MEMBER_NFT_LOGIC; + /************************************************************************** * Storage Variables **************************************************************************/ @@ -66,7 +74,9 @@ abstract contract SuperTokenFactoryBase is ISuperfluid host, ISuperToken superTokenLogic, IConstantOutflowNFT constantOutflowNFTLogic, - IConstantInflowNFT constantInflowNFTLogic + IConstantInflowNFT constantInflowNFTLogic, + IPoolAdminNFT poolAdminNFTLogic, + IPoolMemberNFT poolMemberNFTLogic ) { _host = host; @@ -84,6 +94,10 @@ abstract contract SuperTokenFactoryBase is CONSTANT_INFLOW_NFT_LOGIC = constantInflowNFTLogic; + POOL_ADMIN_NFT_LOGIC = poolAdminNFTLogic; + + POOL_MEMBER_NFT_LOGIC = poolMemberNFTLogic; + // emit SuperTokenLogicCreated event // note that creation here means the setting of the super token logic contract // as the canonical super token logic for the Superfluid framework and not the @@ -140,6 +154,18 @@ abstract contract SuperTokenFactoryBase is if (address(CONSTANT_INFLOW_NFT_LOGIC) != newConstantInflowLogic) { UUPSProxiable(address(_SUPER_TOKEN_LOGIC.CONSTANT_INFLOW_NFT())).updateCode(newConstantInflowLogic); } + + if (address(POOL_ADMIN_NFT_LOGIC) != address(newFactory.POOL_ADMIN_NFT_LOGIC())) { + UUPSProxiable(address(_SUPER_TOKEN_LOGIC.POOL_ADMIN_NFT())).updateCode( + address(newFactory.POOL_ADMIN_NFT_LOGIC()) + ); + } + + if (address(POOL_MEMBER_NFT_LOGIC) != address(newFactory.POOL_MEMBER_NFT_LOGIC())) { + UUPSProxiable(address(_SUPER_TOKEN_LOGIC.POOL_MEMBER_NFT())).updateCode( + address(newFactory.POOL_MEMBER_NFT_LOGIC()) + ); + } } /************************************************************************** @@ -397,13 +423,17 @@ contract SuperTokenFactory is SuperTokenFactoryBase ISuperfluid host, ISuperToken superTokenLogic, IConstantOutflowNFT constantOutflowNFTLogic, - IConstantInflowNFT constantInflowNFTLogic + IConstantInflowNFT constantInflowNFTLogic, + IPoolAdminNFT poolAdminNFTLogic, + IPoolMemberNFT poolMemberNFTLogic ) SuperTokenFactoryBase( host, superTokenLogic, constantOutflowNFTLogic, - constantInflowNFTLogic + constantInflowNFTLogic, + poolAdminNFTLogic, + poolMemberNFTLogic ) // solhint-disable-next-line no-empty-blocks {} diff --git a/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol b/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol index 4c32a8c965..2e0925450e 100644 --- a/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol +++ b/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol @@ -19,7 +19,8 @@ import { ISuperToken, ISuperTokenFactory } from "../interfaces/superfluid/ISuperfluid.sol"; - +import { GeneralDistributionAgreementV1 } from "../agreements/gdav1/GeneralDistributionAgreementV1.sol"; +import { SuperfluidUpgradeableBeacon } from "../upgradability/SuperfluidUpgradeableBeacon.sol"; import { CallUtils } from "../libs/CallUtils.sol"; import { BaseRelayRecipient } from "../libs/BaseRelayRecipient.sol"; @@ -308,6 +309,23 @@ contract Superfluid is token.changeAdmin(newAdmin); } + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Superfluid Upgradeable Beacon + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// @inheritdoc ISuperfluid + function updatePoolBeaconLogic(address newLogic) external override onlyGovernance { + GeneralDistributionAgreementV1 gda = GeneralDistributionAgreementV1( + address( + this.getAgreementClass(keccak256("org.superfluid-finance.agreements.GeneralDistributionAgreement.v1")) + ) + ); + SuperfluidUpgradeableBeacon beacon = SuperfluidUpgradeableBeacon(address(gda.superfluidPoolBeacon())); + beacon.upgradeTo(newLogic); + + emit PoolBeaconLogicUpdated(address(beacon), newLogic); + } + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // App Registry //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/packages/ethereum-contracts/contracts/upgradability/BeaconProxiable.sol b/packages/ethereum-contracts/contracts/upgradability/BeaconProxiable.sol new file mode 100644 index 0000000000..ba692cdaad --- /dev/null +++ b/packages/ethereum-contracts/contracts/upgradability/BeaconProxiable.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +abstract contract BeaconProxiable is Initializable { + + // allows to mark logic contracts as initialized + // solhint-disable-next-line no-empty-blocks + function castrate() external initializer { } + + /** + * @dev Proxiable UUID marker function, this would help to avoid wrong logic + * contract to be used for upgrading. + */ + function proxiableUUID() public pure virtual returns (bytes32); +} \ No newline at end of file diff --git a/packages/ethereum-contracts/contracts/upgradability/SuperfluidUpgradeableBeacon.sol b/packages/ethereum-contracts/contracts/upgradability/SuperfluidUpgradeableBeacon.sol new file mode 100644 index 0000000000..126fc11e95 --- /dev/null +++ b/packages/ethereum-contracts/contracts/upgradability/SuperfluidUpgradeableBeacon.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import { + UpgradeableBeacon +} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import { BeaconProxiable } from "./BeaconProxiable.sol"; + +contract SuperfluidUpgradeableBeacon is UpgradeableBeacon { + error ZERO_ADDRESS_IMPLEMENTATION(); // 0x80883162 + error INCOMPATIBLE_LOGIC(); // 0x5af2144c + error NO_PROXY_LOOP(); // 0z66750bca + + constructor(address implementation_) UpgradeableBeacon(implementation_) {} + + function upgradeTo(address newImplementation) public override onlyOwner { + if (newImplementation == address(0)) { + revert ZERO_ADDRESS_IMPLEMENTATION(); + } + + if (newImplementation == address(this)) { + revert NO_PROXY_LOOP(); + } + + if (BeaconProxiable(newImplementation).proxiableUUID() != BeaconProxiable(implementation()).proxiableUUID()) { + revert INCOMPATIBLE_LOGIC(); + } + + super.upgradeTo(newImplementation); + } +} diff --git a/packages/ethereum-contracts/contracts/upgradability/UUPSProxiable.sol b/packages/ethereum-contracts/contracts/upgradability/UUPSProxiable.sol index 7a8f04dd10..1cc982cce6 100644 --- a/packages/ethereum-contracts/contracts/upgradability/UUPSProxiable.sol +++ b/packages/ethereum-contracts/contracts/upgradability/UUPSProxiable.sol @@ -19,7 +19,7 @@ abstract contract UUPSProxiable is Initializable { function updateCode(address newAddress) external virtual; - // allows to mark logic contracts as initialized in order to reduce the attack surface + // allows to mark logic contracts as initialized // solhint-disable-next-line no-empty-blocks function castrate() external initializer { } diff --git a/packages/ethereum-contracts/contracts/utils/BatchLiquidator.sol b/packages/ethereum-contracts/contracts/utils/BatchLiquidator.sol index 3c6b063e1f..77603cdc72 100644 --- a/packages/ethereum-contracts/contracts/utils/BatchLiquidator.sol +++ b/packages/ethereum-contracts/contracts/utils/BatchLiquidator.sol @@ -2,7 +2,8 @@ pragma solidity 0.8.19; import { - ISuperfluid, ISuperAgreement, ISuperToken, IConstantFlowAgreementV1 + ISuperfluid, ISuperAgreement, ISuperToken, ISuperfluidPool, + IConstantFlowAgreementV1, IGeneralDistributionAgreementV1 } from "../interfaces/superfluid/ISuperfluid.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -14,54 +15,47 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; */ contract BatchLiquidator { + enum FlowType { + ConstantFlowAgreement, + GeneralDistributionAgreement + } - error ARRAY_SIZES_DIFFERENT(); + struct FlowLiquidationData { + FlowType agreementOperation; + address sender; + address receiver; + } address public immutable host; address public immutable cfa; + address public immutable gda; - constructor(address host_, address cfa_) { + constructor(address host_) { host = host_; - cfa = cfa_; + cfa = address( + ISuperfluid(host).getAgreementClass(keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1")) + ); + gda = address( + ISuperfluid(host).getAgreementClass( + keccak256("org.superfluid-finance.agreements.GeneralDistributionAgreement.v1") + ) + ); } /** * @dev Delete flows in batch * @param superToken - The super token the flows belong to. - * @param senders - List of senders. - * @param receivers - Corresponding list of receivers. - * @return nSuccess - Number of succeeded deletions. + * @param data - The array of flow data to be deleted. */ - function deleteFlows( - address superToken, - address[] calldata senders, address[] calldata receivers - ) external returns (uint nSuccess) { - uint256 length = senders.length; - if(length != receivers.length) revert ARRAY_SIZES_DIFFERENT(); - for (uint256 i; i < length;) { + function deleteFlows(address superToken, FlowLiquidationData[] memory data) external { + for (uint256 i; i < data.length;) { // We tolerate any errors occured during liquidations. // It could be due to flow had been liquidated by others. - // solhint-disable-next-line avoid-low-level-calls - (bool success,) = address(host).call( - abi.encodeCall( - ISuperfluid(host).callAgreement, - ( - ISuperAgreement(cfa), - abi.encodeCall( - IConstantFlowAgreementV1(cfa).deleteFlow, - ( - ISuperToken(superToken), - senders[i], - receivers[i], - new bytes(0) - ) - ), - new bytes(0) - ) - ) - ); - if (success) ++nSuccess; - unchecked { i++; } + _deleteFlow(superToken, data[i]); + + unchecked { + i++; + } } // If the liquidation(s) resulted in any super token @@ -78,27 +72,14 @@ contract BatchLiquidator { } } - // single flow delete with check for success - function deleteFlow(address superToken, address sender, address receiver) external { + /** + * @dev Delete a single flow + * @param superToken - The super token the flow belongs to. + * @param data - The flow data to be deleted. + */ + function deleteFlow(address superToken, FlowLiquidationData memory data) external { /* solhint-disable */ - (bool success, bytes memory returndata) = address(host).call( - abi.encodeCall( - ISuperfluid(host).callAgreement, - ( - ISuperAgreement(cfa), - abi.encodeCall( - IConstantFlowAgreementV1(cfa).deleteFlow, - ( - ISuperToken(superToken), - sender, - receiver, - new bytes(0) - ) - ), - new bytes(0) - ) - ) - ); + (bool success, bytes memory returndata) = _deleteFlow(superToken, data); if (!success) { if (returndata.length == 0) revert(); // solhint-disable @@ -119,4 +100,41 @@ contract BatchLiquidator { } } } + + function _deleteFlow(address superToken, FlowLiquidationData memory data) + internal + returns (bool success, bytes memory returndata) + { + if (data.agreementOperation == FlowType.ConstantFlowAgreement) { + // solhint-disable-next-line avoid-low-level-calls + (success, returndata) = address(host).call( + abi.encodeCall( + ISuperfluid(host).callAgreement, + ( + ISuperAgreement(cfa), + abi.encodeCall( + IConstantFlowAgreementV1(cfa).deleteFlow, + (ISuperToken(superToken), data.sender, data.receiver, new bytes(0)) + ), + new bytes(0) + ) + ) + ); + } else { + // solhint-disable-next-line avoid-low-level-calls + (success, returndata) = address(host).call( + abi.encodeCall( + ISuperfluid(host).callAgreement, + ( + ISuperAgreement(gda), + abi.encodeCall( + IGeneralDistributionAgreementV1(gda).distributeFlow, + (ISuperToken(superToken), data.sender, ISuperfluidPool(data.receiver), 0, new bytes(0)) + ), + new bytes(0) + ) + ) + ); + } + } } diff --git a/packages/ethereum-contracts/contracts/utils/GDAv1Forwarder.sol b/packages/ethereum-contracts/contracts/utils/GDAv1Forwarder.sol new file mode 100644 index 0000000000..dd5458d8f3 --- /dev/null +++ b/packages/ethereum-contracts/contracts/utils/GDAv1Forwarder.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import { ISuperfluid, ISuperfluidToken } from "../interfaces/superfluid/ISuperfluid.sol"; +import { ISuperfluidPool } from "../agreements/gdav1/SuperfluidPool.sol"; +import { + IGeneralDistributionAgreementV1, + PoolConfig +} from "../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; +import { ForwarderBase } from "./ForwarderBase.sol"; + +/** + * @title GDAv1Forwarder + * @author Superfluid + * The GDAv1Forwarder contract provides an easy to use interface to + * GeneralDistributionAgreementV1 specific functionality of Super Tokens. + * Instances of this contract can operate on the protocol only if configured as "trusted forwarder" + * by protocol governance. + */ +contract GDAv1Forwarder is ForwarderBase { + IGeneralDistributionAgreementV1 internal immutable _gda; + + // is tied to a specific instance of host and agreement contracts at deploy time + constructor(ISuperfluid host) ForwarderBase(host) { + _gda = IGeneralDistributionAgreementV1( + address( + _host.getAgreementClass(keccak256("org.superfluid-finance.agreements.GeneralDistributionAgreement.v1")) + ) + ); + } + + /** + * @dev Creates a new Superfluid Pool. + * @param token The Super Token address. + * @param admin The pool admin address. + * @param config The pool configuration (see PoolConfig in IGeneralDistributionAgreementV1.sol) + * @return success A boolean value indicating whether the pool was created successfully. + * @return pool The address of the deployed Superfluid Pool + */ + function createPool(ISuperfluidToken token, address admin, PoolConfig memory config) + external + returns (bool success, ISuperfluidPool pool) + { + pool = _gda.createPool(token, admin, config); + success = true; + } + + /** + * @dev Updates the units of a pool member. + * @param pool The Superfluid Pool to update. + * @param memberAddress The address of the member to update. + * @param newUnits The new units of the member. + * @param userData User-specific data. + */ + function updateMemberUnits(ISuperfluidPool pool, address memberAddress, uint128 newUnits, bytes memory userData) + external + returns (bool success) + { + bytes memory callData = abi.encodeCall(_gda.updateMemberUnits, (pool, memberAddress, newUnits, new bytes(0))); + + return _forwardBatchCall(address(_gda), callData, userData); + } + + /** + * @dev Claims all tokens from the pool. + * @param pool The Superfluid Pool to claim from. + * @param memberAddress The address of the member to claim for. + * @param userData User-specific data. + */ + function claimAll(ISuperfluidPool pool, address memberAddress, bytes memory userData) + external + returns (bool success) + { + bytes memory callData = abi.encodeCall(_gda.claimAll, (pool, memberAddress, new bytes(0))); + + return _forwardBatchCall(address(_gda), callData, userData); + } + + /** + * @dev Connects a pool member to `pool`. + * @param pool The Superfluid Pool to connect. + * @param userData User-specific data. + * @return A boolean value indicating whether the connection was successful. + */ + function connectPool(ISuperfluidPool pool, bytes memory userData) external returns (bool) { + bytes memory callData = abi.encodeCall(_gda.connectPool, (pool, new bytes(0))); + + return _forwardBatchCall(address(_gda), callData, userData); + } + + /** + * @dev Disconnects a pool member from `pool`. + * @param pool The Superfluid Pool to disconnect. + * @param userData User-specific data. + * @return A boolean value indicating whether the disconnection was successful. + */ + function disconnectPool(ISuperfluidPool pool, bytes memory userData) external returns (bool) { + bytes memory callData = abi.encodeCall(_gda.disconnectPool, (pool, new bytes(0))); + + return _forwardBatchCall(address(_gda), callData, userData); + } + + /** + * @dev Tries to distribute `requestedAmount` amount of `token` from `from` to `pool`. + * @param token The Super Token address. + * @param from The address from which to distribute tokens. + * @param pool The Superfluid Pool address. + * @param requestedAmount The amount of tokens to distribute. + * @param userData User-specific data. + * @return A boolean value indicating whether the distribution was successful. + */ + function distribute( + ISuperfluidToken token, + address from, + ISuperfluidPool pool, + uint256 requestedAmount, + bytes memory userData + ) external returns (bool) { + bytes memory callData = abi.encodeCall(_gda.distribute, (token, from, pool, requestedAmount, new bytes(0))); + + return _forwardBatchCall(address(_gda), callData, userData); + } + + /** + * @dev Tries to distribute flow at `requestedFlowRate` of `token` from `from` to `pool`. + * @param token The Super Token address. + * @param from The address from which to distribute tokens. + * @param pool The Superfluid Pool address. + * @param requestedFlowRate The flow rate of tokens to distribute. + * @param userData User-specific data. + * @return A boolean value indicating whether the distribution was successful. + */ + function distributeFlow( + ISuperfluidToken token, + address from, + ISuperfluidPool pool, + int96 requestedFlowRate, + bytes memory userData + ) external returns (bool) { + bytes memory callData = + abi.encodeCall(_gda.distributeFlow, (token, from, pool, requestedFlowRate, new bytes(0))); + + return _forwardBatchCall(address(_gda), callData, userData); + } + + /** + * @dev Checks if the specified account is a pool. + * @param token The Super Token address. + * @param account The account address to check. + * @return A boolean value indicating whether the account is a pool. + */ + function isPool(ISuperfluidToken token, address account) external view virtual returns (bool) { + return _gda.isPool(token, account); + } + + /** + * @dev Gets the GDA net flow rate for the specified account. + * @param token The Super Token address. + * @param account The account address. + * @return The gda net flow rate for the account. + */ + function getNetFlow(ISuperfluidToken token, address account) external view returns (int96) { + return _gda.getNetFlow(token, account); + } + + /** + * @dev Gets the flow rate of tokens between the specified accounts. + * @param token The Super Token address. + * @param from The sender address. + * @param to The receiver address (the pool address). + * @return The flow distribution flow rate + */ + function getFlowDistributionFlowRate(ISuperfluidToken token, address from, ISuperfluidPool to) + external + view + returns (int96) + { + return _gda.getFlowRate(token, from, to); + } + + /** + * @dev Gets the pool adjustment flow rate for the specified pool. + * @param pool The pool address. + * @return The pool adjustment flow rate. + */ + function getPoolAdjustmentFlowRate(address pool) external view virtual returns (int96) { + return _gda.getPoolAdjustmentFlowRate(pool); + } + + /** + * @dev Estimates the actual flow rate for flow distribution to the specified pool. + * @param token The Super Token address. + * @param from The sender address. + * @param to The pool address. + * @param requestedFlowRate The requested flow rate. + * @return actualFlowRate + * @return totalDistributionFlowRate + */ + function estimateFlowDistributionActualFlowRate( + ISuperfluidToken token, + address from, + ISuperfluidPool to, + int96 requestedFlowRate + ) external view returns (int96 actualFlowRate, int96 totalDistributionFlowRate) { + return _gda.estimateFlowDistributionActualFlowRate(token, from, to, requestedFlowRate); + } + + /** + * @dev Estimates the actual amount for distribution to the specified pool. + * @param token The Super Token address. + * @param from The sender address. + * @param to The pool address. + * @param requestedAmount The requested amount. + * @return actualAmount The actual amount for distribution. + */ + function estimateDistributionActualAmount( + ISuperfluidToken token, + address from, + ISuperfluidPool to, + uint256 requestedAmount + ) external view returns (uint256 actualAmount) { + return _gda.estimateDistributionActualAmount(token, from, to, requestedAmount); + } + + /** + * @dev Checks if the specified member is connected to the pool. + * @param pool The Superfluid Pool address. + * @param member The member address. + * @return A boolean value indicating whether the member is connected to the pool. + */ + function isMemberConnected(ISuperfluidPool pool, address member) external view returns (bool) { + return _gda.isMemberConnected(pool, member); + } + + /** + * @dev Gets the pool adjustment flow information for the specified pool. + * @param pool The pool address. + * @return The pool admin, pool ID, and pool adjustment flow rate. + */ + function getPoolAdjustmentFlowInfo(ISuperfluidPool pool) external view virtual returns (address, bytes32, int96) { + return _gda.getPoolAdjustmentFlowInfo(pool); + } +} diff --git a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeployer.sol b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeployer.sol index 45a876d1d2..f96adc7883 100644 --- a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeployer.sol +++ b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeployer.sol @@ -41,118 +41,14 @@ contract SuperfluidFrameworkDeployer is SuperfluidFrameworkDeploymentSteps { uint256 minBondDuration; } - error DEPLOY_AGREEMENTS_REQUIRES_DEPLOY_CORE(); - error DEPLOY_PERIPHERALS_REQUIRES_DEPLOY_CORE(); - error DEPLOY_PERIPHERALS_REQUIRES_DEPLOY_AGREEMENTS(); - error DEPLOY_SUPER_TOKEN_CONTRACTS_REQUIRES_DEPLOY_CORE(); - error DEPLOY_SUPER_TOKEN_REQUIRES_1820(); - error DEPLOY_SUPER_TOKEN_REQUIRES_DEPLOY_SUPER_TOKEN_CONTRACTS(); - error DEPLOY_TOGA_REQUIRES_1820(); - error RESOLVER_LIST_REQUIRES_DEPLOY_PERIPHERALS(); - /// @notice Deploys the Superfluid Framework (Test) /// @dev This uses default configurations for the framework. /// NOTE: ERC1820 must be deployed as a prerequisite before calling this function. function deployTestFramework() external { // Default Configs - TestFrameworkConfigs memory configs = TestFrameworkConfigs({ - nonUpgradeable: DEFAULT_NON_UPGRADEABLE, - appWhitelistingEnabled: DEFAULT_APP_WHITELISTING_ENABLED, - trustedForwarders: DEFAULT_TRUSTED_FORWARDERS, - defaultRewardAddress: DEFAULT_REWARD_ADDRESS, - liquidationPeriod: DEFAULT_LIQUIDATION_PERIOD, - patricianPeriod: DEFAULT_PATRICIAN_PERIOD, - minBondDuration: DEFAULT_TOGA_MIN_BOND_DURATION - }); - - _deployTestFramework(configs); - } - - function _deployTestFramework(TestFrameworkConfigs memory configs) internal { - // Deploy Host and Governance - _deployCoreContracts(configs); - - // Initialize Host with Governance address - _initializeHost(); - - // Initialize Governance with Host address and Configs - _initializeGovernance( - configs.defaultRewardAddress, configs.liquidationPeriod, configs.patricianPeriod, configs.trustedForwarders - ); - - // Deploy CFAv1 and IDAv1 - _deployAgreementContracts(); - - // Register the agreements with governance - _registerAgreements(); - - // Deploy NFT Proxy and Logic, SuperToken Logic, SuperTokenFactory Proxy and Logic contracts - _deploySuperTokenContracts(); - - // Set SuperTokenFactory as the canonical contract - _setSuperTokenFactoryInHost(); - - // Deploy Resolver, SuperfluidLoaderV1, CFAv1Forwarder, TOGA, BatchLiquidator contracts - _deployPeripheralContracts(configs); - - // Enable the CFAv1Forwarder as a trusted forwarder via Governance - _enableCFAv1ForwarderAsTrustedForwarder(); - - // Enable the IDAv1Forwarder as a trusted forwarder via Governance - _enableIDAv1ForwarderAsTrustedForwarder(); - - // Set TestGovernance, Superfluid, SuperfluidLoader and CFAv1Forwarder addresses in Resolver - _setAddressesInResolver(); - } - - /// @notice Deploys the core Superfluid contracts - /// @dev Host and Governance - function deployCoreContracts() public { - TestFrameworkConfigs memory configs; - configs.nonUpgradeable = true; - configs.appWhitelistingEnabled = false; - - _deployCoreContracts(configs); - } - - /// @notice Deploys the core Superfluid contracts w/ Configs - /// @dev Host and Governance - /// @param configs the configurations for the framework - function deployCoreContracts(TestFrameworkConfigs memory configs) public { - _deployCoreContracts(configs); - } - - function _deployCoreContracts(TestFrameworkConfigs memory configs) internal { - _deployGovernance(address(this)); - _deployHost(configs.nonUpgradeable, configs.appWhitelistingEnabled); - } - - /// @notice Deploys the Superfluid agreement contracts - /// @dev Deploys Superfluid agreement contracts - /// NOTE: This requires the core contracts to be deployed first. - function deployAgreementContracts() public { - _deployAgreementContracts(); - } - - function _deployAgreementContracts() internal { - if (address(host) == address(0)) revert DEPLOY_AGREEMENTS_REQUIRES_DEPLOY_CORE(); - - _deployCFAv1(); - _deployIDAv1(); - } - - /// @notice Deploys all SuperToken-related contracts - /// @dev Deploys NFT Proxy and Logic, SuperToken Logic, SuperTokenFactory Proxy and Logic contracts - function deploySuperTokenContracts() public { - _deploySuperTokenContracts(); - } - - function _deploySuperTokenContracts() internal { - if (address(host) == address(0)) revert DEPLOY_SUPER_TOKEN_CONTRACTS_REQUIRES_DEPLOY_CORE(); - - _deployNFTProxyAndLogicAndInitialize(); - _deploySuperTokenLogic(); - _deploySuperTokenFactory(); + for (uint256 i = 0; i < getNumSteps(); ++i) { + executeStep(uint8(i)); + } } /// @notice Deploys an ERC20 and a Wrapper Super Token for the ERC20 and lists both in the resolver @@ -178,27 +74,6 @@ contract SuperfluidFrameworkDeployer is SuperfluidFrameworkDeploymentSteps { return _deployWrapperSuperToken(_underlyingName, _underlyingSymbol, _decimals, _mintLimit, _admin); } - /// @notice Deploys an ERC20 and a Wrapper Super Token for the ERC20 and lists both in the resolver - /// @dev SuperToken name and symbol format: `Super ${_underlyingSymbol}` and `${_underlyingSymbol}x`, respectively - /// @param _underlyingName The underlying token name - /// @param _underlyingSymbol The token symbol - /// @param _decimals The token decimals - /// @param _mintLimit The mint limit of the underlying token - /// @return underlyingToken and superToken - function deployWrapperSuperToken( - string calldata _underlyingName, - string calldata _underlyingSymbol, - uint8 _decimals, - uint256 _mintLimit - ) - external - requiresSuperTokenFactory - deploySuperTokenRequires1820 - returns (TestToken underlyingToken, SuperToken superToken) - { - return _deployWrapperSuperToken(_underlyingName, _underlyingSymbol, _decimals, _mintLimit, address(0)); - } - /// @notice Deploys a Native Asset Super Token and lists it in the resolver /// @dev e.g. ETHx, MATICx, AVAXx, etc. The underlying is the Native Asset. /// @param _name The token name @@ -245,8 +120,8 @@ contract SuperfluidFrameworkDeployer is SuperfluidFrameworkDeploymentSteps { function _handleResolverList(bool _listOnResolver, string memory _resolverKey, address _superTokenAddress) internal - requiresResolver { + if (address(testResolver) == address(0)) revert RESOLVER_LIST_REQUIRES_DEPLOY_PERIPHERALS(); if (_listOnResolver) { testResolver.set(_resolverKey, address(_superTokenAddress)); } @@ -286,65 +161,6 @@ contract SuperfluidFrameworkDeployer is SuperfluidFrameworkDeploymentSteps { _handleResolverList(true, superTokenKey, address(superToken)); } - /// @notice Deploys all peripheral Superfluid contracts - /// @dev Deploys Resolver, SuperfluidLoaderV1, CFAv1Forwarder, TOGA, BatchLiquidator contracts - function deployPeripheralContracts() public { - TestFrameworkConfigs memory configs; - configs.minBondDuration = DEFAULT_TOGA_MIN_BOND_DURATION; - - _deployPeripheralContracts(configs); - } - - /// @notice Deploys all peripheral Superfluid contracts with configs - /// @dev Deploys Resolver, SuperfluidLoaderV1, CFAv1Forwarder, TOGA, BatchLiquidator contracts - function deployPeripheralContracts(TestFrameworkConfigs memory configs) public { - _deployPeripheralContracts(configs); - } - - function _deployPeripheralContracts(TestFrameworkConfigs memory configs) internal { - if (address(host) == address(0)) revert DEPLOY_PERIPHERALS_REQUIRES_DEPLOY_CORE(); - - _deployTestResolver(address(this)); - _deploySuperfluidLoader(); - - // Set the deployer of this contract as an admin of the resolver - // So that they can add other admins and set addresses - testResolver.addAdmin(msg.sender); - - _deployCFAv1Forwarder(); - _deployIDAv1Forwarder(); - _deployTOGA(configs.minBondDuration); - - if (address(cfaV1) == address(0)) revert DEPLOY_PERIPHERALS_REQUIRES_DEPLOY_AGREEMENTS(); - _deployBatchLiquidator(); - } - - function _deployTOGA(uint256 _minBondDuration) internal override deployTogaRequires1820 { - super._deployTOGA(_minBondDuration); - } - - //// JS-Specific Functions //// - function getNumSteps() external pure returns (uint8) { - return _getNumSteps(); - } - - function executeStep(uint8 step) external { - _executeStep(step); - } - - function _is1820Deployed() internal view returns (bool) { - uint256 codeSize; - assembly { - codeSize := extcodesize(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24) - } - return codeSize != 0; - } - - modifier requiresResolver() { - if (address(testResolver) == address(0)) revert RESOLVER_LIST_REQUIRES_DEPLOY_PERIPHERALS(); - _; - } - modifier requiresSuperTokenFactory() { if (address(superTokenFactory) == address(0)) revert DEPLOY_SUPER_TOKEN_REQUIRES_DEPLOY_SUPER_TOKEN_CONTRACTS(); _; @@ -354,9 +170,4 @@ contract SuperfluidFrameworkDeployer is SuperfluidFrameworkDeploymentSteps { if (!_is1820Deployed()) revert DEPLOY_SUPER_TOKEN_REQUIRES_1820(); _; } - - modifier deployTogaRequires1820() { - if (!_is1820Deployed()) revert DEPLOY_TOGA_REQUIRES_1820(); - _; - } } diff --git a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol index 1b3e899245..b207720f50 100644 --- a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol +++ b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol @@ -6,12 +6,16 @@ pragma solidity >=0.8.11; import { CFAv1Forwarder } from "./CFAv1Forwarder.sol"; import { IDAv1Forwarder } from "./IDAv1Forwarder.sol"; +import { GDAv1Forwarder } from "./GDAv1Forwarder.sol"; import { ISuperfluid, ISuperfluidToken, Superfluid } from "../superfluid/Superfluid.sol"; import { TestGovernance } from "./TestGovernance.sol"; import { ConstantFlowAgreementV1 } from "../agreements/ConstantFlowAgreementV1.sol"; import { ConstantOutflowNFT, IConstantOutflowNFT } from "../superfluid/ConstantOutflowNFT.sol"; import { ConstantInflowNFT, IConstantInflowNFT } from "../superfluid/ConstantInflowNFT.sol"; +import { PoolAdminNFT, IPoolAdminNFT } from "../agreements/gdav1/PoolAdminNFT.sol"; +import { PoolMemberNFT, IPoolMemberNFT } from "../agreements/gdav1/PoolMemberNFT.sol"; import { InstantDistributionAgreementV1 } from "../agreements/InstantDistributionAgreementV1.sol"; +import { GeneralDistributionAgreementV1 } from "../agreements/gdav1/GeneralDistributionAgreementV1.sol"; import { SuperTokenFactory } from "../superfluid/SuperTokenFactory.sol"; import { TestToken } from "./TestToken.sol"; import { PureSuperToken } from "../tokens/PureSuperToken.sol"; @@ -19,6 +23,8 @@ import { SETHProxy } from "../tokens/SETH.sol"; import { ISuperToken, SuperToken } from "../superfluid/SuperToken.sol"; import { TestResolver } from "./TestResolver.sol"; import { SuperfluidLoader } from "./SuperfluidLoader.sol"; +import { SuperfluidPool } from "../agreements/gdav1/SuperfluidPool.sol"; +import { SuperfluidUpgradeableBeacon } from "../upgradability/SuperfluidUpgradeableBeacon.sol"; import { UUPSProxy } from "../upgradability/UUPSProxy.sol"; import { BatchLiquidator } from "./BatchLiquidator.sol"; import { TOGA } from "./TOGA.sol"; @@ -52,6 +58,7 @@ contract SuperfluidFrameworkDeploymentSteps { ConstantFlowAgreementV1 cfa; CFAv1Library.InitData cfaLib; InstantDistributionAgreementV1 ida; + GeneralDistributionAgreementV1 gda; IDAv1Library.InitData idaLib; SuperTokenFactory superTokenFactory; ISuperToken superTokenLogic; @@ -61,6 +68,8 @@ contract SuperfluidFrameworkDeploymentSteps { SuperfluidLoader superfluidLoader; CFAv1Forwarder cfaV1Forwarder; IDAv1Forwarder idaV1Forwarder; + GDAv1Forwarder gdaV1Forwarder; + BatchLiquidator batchLiquidator; TOGA toga; } @@ -75,12 +84,18 @@ contract SuperfluidFrameworkDeploymentSteps { ConstantFlowAgreementV1 internal cfaV1Logic; InstantDistributionAgreementV1 internal idaV1; InstantDistributionAgreementV1 internal idaV1Logic; + GeneralDistributionAgreementV1 internal gdaV1; + GeneralDistributionAgreementV1 internal gdaV1Logic; // SuperToken-related Contracts ConstantOutflowNFT internal constantOutflowNFTLogic; ConstantInflowNFT internal constantInflowNFTLogic; ConstantOutflowNFT internal constantOutflowNFT; ConstantInflowNFT internal constantInflowNFT; + PoolAdminNFT internal poolAdminNFTLogic; + PoolMemberNFT internal poolMemberNFTLogic; + PoolAdminNFT internal poolAdminNFT; + PoolMemberNFT internal poolMemberNFT; ISuperToken internal superTokenLogic; SuperTokenFactory internal superTokenFactory; SuperTokenFactory internal superTokenFactoryLogic; @@ -90,213 +105,18 @@ contract SuperfluidFrameworkDeploymentSteps { SuperfluidLoader internal superfluidLoader; CFAv1Forwarder internal cfaV1Forwarder; IDAv1Forwarder internal idaV1Forwarder; + GDAv1Forwarder internal gdaV1Forwarder; BatchLiquidator internal batchLiquidator; TOGA internal toga; - function _deployGovernance(address newOwner) internal { - // Deploy TestGovernance. Needs initialization later. - testGovernance = SuperfluidGovDeployerLibrary.deployTestGovernance(); - - SuperfluidGovDeployerLibrary.transferOwnership(testGovernance, newOwner); - } - - function _deployHost(bool nonUpgradable, bool appWhiteListingEnabled) internal { - host = SuperfluidHostDeployerLibrary.deploySuperfluidHost(nonUpgradable, appWhiteListingEnabled); - } - - function _initializeHost() internal { - host.initialize(testGovernance); - } - - function _initializeGovernance( - address defaultRewardAddress, - uint256 defaultLiquidationPeriod, - uint256 defaultPatricianPeriod, - address[] memory defaultTrustedForwarders - ) internal { - testGovernance.initialize( - host, defaultRewardAddress, defaultLiquidationPeriod, defaultPatricianPeriod, defaultTrustedForwarders - ); - } - - function _deployHostAndInitializeHostAndGovernance(bool nonUpgradable, bool appWhiteListingEnabled) internal { - // Deploy Host - _deployHost(nonUpgradable, appWhiteListingEnabled); - - _initializeHost(); - - _initializeGovernance( - DEFAULT_REWARD_ADDRESS, DEFAULT_LIQUIDATION_PERIOD, DEFAULT_PATRICIAN_PERIOD, DEFAULT_TRUSTED_FORWARDERS - ); - } - - function _deployCFAv1() internal { - cfaV1Logic = SuperfluidCFAv1DeployerLibrary.deployConstantFlowAgreementV1(host); - } - - function _deployIDAv1() internal { - idaV1Logic = SuperfluidIDAv1DeployerLibrary.deployInstantDistributionAgreementV1(host); - } - - function _deployAgreements() internal { - _deployCFAv1(); - _deployIDAv1(); - } - - function _deployAgreementsAndRegister() internal { - _deployAgreements(); - _registerAgreements(); - } - - function _registerAgreements() internal { - // we set the canonical address based on host.getAgreementClass() because - // in the upgradeable case, we create a new proxy contract in the function - // and set it as the canonical agreement. - testGovernance.registerAgreementClass(host, address(cfaV1Logic)); - cfaV1 = ConstantFlowAgreementV1(address(host.getAgreementClass(cfaV1Logic.agreementType()))); - testGovernance.registerAgreementClass(host, address(idaV1Logic)); - idaV1 = InstantDistributionAgreementV1(address(host.getAgreementClass(idaV1Logic.agreementType()))); - } - - function _deployCFAv1Forwarder() internal { - cfaV1Forwarder = CFAv1ForwarderDeployerLibrary.deployCFAv1Forwarder(host); - } - - function _enableCFAv1ForwarderAsTrustedForwarder() internal { - testGovernance.enableTrustedForwarder(host, ISuperfluidToken(address(0)), address(cfaV1Forwarder)); - } - - function _deployCFAv1ForwarderAndEnable() internal { - _deployCFAv1Forwarder(); - _enableCFAv1ForwarderAsTrustedForwarder(); - } - - function _deployIDAv1Forwarder() internal { - idaV1Forwarder = IDAv1ForwarderDeployerLibrary.deployIDAv1Forwarder(host); - } - - function _enableIDAv1ForwarderAsTrustedForwarder() internal { - testGovernance.enableTrustedForwarder(host, ISuperfluidToken(address(0)), address(idaV1Forwarder)); - } - - function _deployIDAv1ForwarderAndEnable() internal { - _deployIDAv1Forwarder(); - _enableIDAv1ForwarderAsTrustedForwarder(); - } - - function _deployNFTProxyAndLogicAndInitialize() internal { - // Deploy canonical Constant Outflow NFT proxy contract - UUPSProxy constantOutflowNFTProxy = ProxyDeployerLibrary.deployUUPSProxy(); - - // Deploy canonical Constant Outflow NFT proxy contract - UUPSProxy constantInflowNFTProxy = ProxyDeployerLibrary.deployUUPSProxy(); - - // Deploy canonical Constant Outflow NFT logic contract - constantOutflowNFTLogic = SuperfluidNFTLogicDeployerLibrary.deployConstantOutflowNFT( - host, IConstantInflowNFT(address(constantInflowNFTProxy)) - ); - - // Initialize Constant Outflow NFT logic contract - constantOutflowNFTLogic.castrate(); - - // Deploy canonical Constant Inflow NFT logic contract - constantInflowNFTLogic = SuperfluidNFTLogicDeployerLibrary.deployConstantInflowNFT( - host, IConstantOutflowNFT(address(constantOutflowNFTProxy)) - ); - - // Initialize Constant Inflow NFT logic contract - constantInflowNFTLogic.castrate(); - - // Initialize COFNFT proxy contract - constantOutflowNFTProxy.initializeProxy(address(constantOutflowNFTLogic)); - - // Initialize CIFNFT proxy contract - constantInflowNFTProxy.initializeProxy(address(constantInflowNFTLogic)); - - // // Initialize COFNFT proxy contract - IConstantOutflowNFT(address(constantOutflowNFTProxy)).initialize("Constant Outflow NFT", "COF"); - - // // Initialize CIFNFT proxy contract - IConstantInflowNFT(address(constantInflowNFTProxy)).initialize("Constant Inflow NFT", "CIF"); - - constantOutflowNFT = ConstantOutflowNFT(address(constantOutflowNFTProxy)); - constantInflowNFT = ConstantInflowNFT(address(constantInflowNFTProxy)); - } - - function _deploySuperTokenLogicAndSuperTokenFactoryAndUpdateContracts() internal { - _deploySuperTokenLogicAndSuperTokenFactory(); - _setSuperTokenFactoryInHost(); - } - - function _deploySuperTokenLogicAndSuperTokenFactory() internal { - _deploySuperTokenLogic(); - _deploySuperTokenFactory(); - } - - function _deploySuperTokenLogic() internal { - // Deploy canonical SuperToken logic contract - superTokenLogic = SuperToken( - SuperTokenDeployerLibrary.deploySuperTokenLogic( - host, IConstantOutflowNFT(address(constantOutflowNFT)), IConstantInflowNFT(address(constantInflowNFT)) - ) - ); - } - - function _deploySuperTokenFactory() internal { - superTokenFactoryLogic = SuperfluidPeripheryDeployerLibrary.deploySuperTokenFactory( - host, superTokenLogic, constantOutflowNFTLogic, constantInflowNFTLogic - ); - } - - function _setSuperTokenFactoryInHost() internal { - // 'Update' code with Governance and register SuperTokenFactory with Superfluid - testGovernance.updateContracts(host, address(0), new address[](0), address(superTokenFactoryLogic)); - - // we set the canonical address based on host.getSuperTokenFactory() because - // in the upgradeable case, we create a new proxy contract in the function - // and set it as the canonical supertokenfactory. - superTokenFactory = SuperTokenFactory(address(host.getSuperTokenFactory())); - } - - function _deployTestResolver(address resolverAdmin) internal { - testResolver = SuperfluidPeripheryDeployerLibrary.deployTestResolver(resolverAdmin); - } - - function _deploySuperfluidLoader() internal { - superfluidLoader = SuperfluidLoaderDeployerLibrary.deploySuperfluidLoader(testResolver); - } - - function _deployTestResolverAndSuperfluidLoaderAndSet(address resolverAdmin) internal { - _deployTestResolver(resolverAdmin); - _deploySuperfluidLoader(); - - _setAddressesInResolver(); - } - - function _setAddressesInResolver() internal { - // Register Governance with Resolver - testResolver.set("TestGovernance.test", address(testGovernance)); - - // Register Superfluid with Resolver - testResolver.set("Superfluid.test", address(host)); - - // Register SuperfluidLoader with Resolver - testResolver.set("SuperfluidLoader-v1", address(superfluidLoader)); - - // Register CFAv1Forwarder with Resolver - testResolver.set("CFAv1Forwarder", address(cfaV1Forwarder)); - - // Register IDAv1Forwarder with Resolver - testResolver.set("IDAv1Forwarder", address(idaV1Forwarder)); - } - - function _deployBatchLiquidator() internal { - batchLiquidator = new BatchLiquidator(address(host), address(cfaV1)); - } - - function _deployTOGA(uint256 minBondDuration) internal virtual { - toga = new TOGA(host, minBondDuration); - } + error DEPLOY_AGREEMENTS_REQUIRES_DEPLOY_CORE(); + error DEPLOY_PERIPHERALS_REQUIRES_DEPLOY_CORE(); + error DEPLOY_PERIPHERALS_REQUIRES_DEPLOY_AGREEMENTS(); + error DEPLOY_TOGA_REQUIRES_1820(); + error DEPLOY_SUPER_TOKEN_CONTRACTS_REQUIRES_DEPLOY_CORE(); + error DEPLOY_SUPER_TOKEN_REQUIRES_1820(); + error DEPLOY_SUPER_TOKEN_REQUIRES_DEPLOY_SUPER_TOKEN_CONTRACTS(); + error RESOLVER_LIST_REQUIRES_DEPLOY_PERIPHERALS(); /// @notice Fetches the framework contracts function getFramework() external view returns (Framework memory sf) { @@ -307,6 +127,7 @@ contract SuperfluidFrameworkDeploymentSteps { cfaLib: CFAv1Library.InitData(host, cfaV1), ida: idaV1, idaLib: IDAv1Library.InitData(host, idaV1), + gda: gdaV1, superTokenFactory: superTokenFactory, superTokenLogic: superTokenLogic, constantOutflowNFT: constantOutflowNFT, @@ -315,6 +136,8 @@ contract SuperfluidFrameworkDeploymentSteps { superfluidLoader: superfluidLoader, cfaV1Forwarder: cfaV1Forwarder, idaV1Forwarder: idaV1Forwarder, + gdaV1Forwarder: gdaV1Forwarder, + batchLiquidator: batchLiquidator, toga: toga }); return sf; @@ -327,55 +150,270 @@ contract SuperfluidFrameworkDeploymentSteps { testGovernance.transferOwnership(newOwner); } - function _getNumSteps() internal pure returns (uint8) { + function getNumSteps() public pure returns (uint8) { return 8; } - function _executeStep(uint8 step) internal { + function executeStep(uint8 step) public { if (step != currentStep) revert("Incorrect step"); + // CORE CONTRACTS if (step == 0) { // Deploy Superfluid Governance - _deployGovernance(address(this)); + // Deploy TestGovernance. Needs initialization later. + testGovernance = SuperfluidGovDeployerLibrary.deployTestGovernance(); + + SuperfluidGovDeployerLibrary.transferOwnership(testGovernance, address(this)); } else if (step == 1) { - // Deploy Superfluid Host - _deployHostAndInitializeHostAndGovernance(true, false); + // Deploy Host + // _deployHost(nonUpgradable, appWhiteListingEnabled); + host = SuperfluidHostDeployerLibrary.deploySuperfluidHost(true, false); + + // _initializeHost(); + host.initialize(testGovernance); + + // _initializeGovernance( + // DEFAULT_REWARD_ADDRESS, DEFAULT_LIQUIDATION_PERIOD, DEFAULT_PATRICIAN_PERIOD, + // DEFAULT_TRUSTED_FORWARDERS + // ); + testGovernance.initialize( + host, + DEFAULT_REWARD_ADDRESS, + DEFAULT_LIQUIDATION_PERIOD, + DEFAULT_PATRICIAN_PERIOD, + DEFAULT_TRUSTED_FORWARDERS + ); } else if (step == 2) { + // AGREEMENT CONTRACTS // Deploy Superfluid CFA, IDA, GDA - _deployAgreementsAndRegister(); + + if (address(host) == address(0)) revert DEPLOY_AGREEMENTS_REQUIRES_DEPLOY_CORE(); + + // _deployAgreementContracts(); + // _deployCFAv1(); + cfaV1Logic = SuperfluidCFAv1DeployerLibrary.deployConstantFlowAgreementV1(host); + + // _deployIDAv1(); + idaV1Logic = SuperfluidIDAv1DeployerLibrary.deployInstantDistributionAgreementV1(host); + + // _deployGDAv1(); + gdaV1Logic = SuperfluidGDAv1DeployerLibrary.deployGeneralDistributionAgreementV1(host); + + // _registerAgreements(); + // we set the canonical address based on host.getAgreementClass() because + // in the upgradeable case, we create a new proxy contract in the function + // and set it as the canonical agreement. + testGovernance.registerAgreementClass(host, address(cfaV1Logic)); + cfaV1 = ConstantFlowAgreementV1(address(host.getAgreementClass(cfaV1Logic.agreementType()))); + testGovernance.registerAgreementClass(host, address(idaV1Logic)); + idaV1 = InstantDistributionAgreementV1(address(host.getAgreementClass(idaV1Logic.agreementType()))); + testGovernance.registerAgreementClass(host, address(gdaV1Logic)); + gdaV1 = GeneralDistributionAgreementV1(address(host.getAgreementClass(gdaV1Logic.agreementType()))); } else if (step == 3) { + // PERIPHERAL CONTRACTS: FORWARDERS // Deploy CFAv1Forwarder - _deployCFAv1ForwarderAndEnable(); + // _deployCFAv1Forwarder() + cfaV1Forwarder = CFAv1ForwarderDeployerLibrary.deployCFAv1Forwarder(host); + // _enableCFAv1ForwarderAsTrustedForwarder() + testGovernance.enableTrustedForwarder(host, ISuperfluidToken(address(0)), address(cfaV1Forwarder)); // Deploy IDAv1Forwarder - _deployIDAv1ForwarderAndEnable(); + // _deployIDAv1Forwarder(); + idaV1Forwarder = IDAv1ForwarderDeployerLibrary.deployIDAv1Forwarder(host); + // _enableIDAv1ForwarderAsTrustedForwarder(); + testGovernance.enableTrustedForwarder(host, ISuperfluidToken(address(0)), address(idaV1Forwarder)); // Deploy GDAv1Forwarder - // TODO - // solhint-disable-next-line no-empty-blocks + // _deployGDAv1Forwarder(); + gdaV1Forwarder = GDAv1ForwarderDeployerLibrary.deployGDAv1Forwarder(host); + // _enableGDAv1ForwarderAsTrustedForwarder(); + testGovernance.enableTrustedForwarder(host, ISuperfluidToken(address(0)), address(gdaV1Forwarder)); } else if (step == 4) { + // PERIPHERAL CONTRACTS: SuperfluidPool Logic // Deploy SuperfluidPool // Initialize GDA with SuperfluidPool beacon + // _deploySuperfluidPoolLogicAndInitializeGDA(); + + /// Deploy SuperfluidPool logic contract + SuperfluidPool superfluidPoolLogic = SuperfluidPoolLogicDeployerLibrary.deploySuperfluidPool(gdaV1); + + // Initialize the logic contract + superfluidPoolLogic.castrate(); + + // Deploy SuperfluidPool beacon + SuperfluidUpgradeableBeacon superfluidPoolBeacon = + ProxyDeployerLibrary.deploySuperfluidUpgradeableBeacon(address(superfluidPoolLogic)); + gdaV1.initialize(superfluidPoolBeacon); + + superfluidPoolBeacon.transferOwnership(address(host)); } else if (step == 5) { + // PERIPHERAL CONTRACTS: NFT Proxy and Logic // Deploy Superfluid NFTs (Proxy and Logic contracts) - _deployNFTProxyAndLogicAndInitialize(); + + if (address(host) == address(0)) revert DEPLOY_SUPER_TOKEN_CONTRACTS_REQUIRES_DEPLOY_CORE(); + // Deploy canonical Constant Outflow NFT proxy contract + UUPSProxy constantOutflowNFTProxy = ProxyDeployerLibrary.deployUUPSProxy(); + + // Deploy canonical Constant Outflow NFT proxy contract + UUPSProxy constantInflowNFTProxy = ProxyDeployerLibrary.deployUUPSProxy(); + + // Deploy canonical Pool Admin NFT proxy contract + UUPSProxy poolAdminNFTProxy = ProxyDeployerLibrary.deployUUPSProxy(); + + // Deploy canonical Pool Member NFT proxy contract + UUPSProxy poolMemberNFTProxy = ProxyDeployerLibrary.deployUUPSProxy(); + + // Deploy canonical Constant Outflow NFT logic contract + constantOutflowNFTLogic = SuperfluidFlowNFTLogicDeployerLibrary.deployConstantOutflowNFT( + host, IConstantInflowNFT(address(constantInflowNFTProxy)) + ); + + // Initialize Constant Outflow NFT logic contract + constantOutflowNFTLogic.castrate(); + + // Deploy canonical Constant Inflow NFT logic contract + constantInflowNFTLogic = SuperfluidFlowNFTLogicDeployerLibrary.deployConstantInflowNFT( + host, IConstantOutflowNFT(address(constantOutflowNFTProxy)) + ); + + // Initialize Constant Inflow NFT logic contract + constantInflowNFTLogic.castrate(); + + // Deploy canonical Pool Admin NFT logic contract + poolAdminNFTLogic = SuperfluidPoolNFTLogicDeployerLibrary.deployPoolAdminNFT(host); + + // Initialize Pool Admin NFT logic contract + poolAdminNFTLogic.castrate(); + + // Deploy canonical Pool Member NFT logic contract + poolMemberNFTLogic = SuperfluidPoolNFTLogicDeployerLibrary.deployPoolMemberNFT(host); + + // Initialize Pool Member NFT logic contract + poolMemberNFTLogic.castrate(); + + // Initialize COFNFT proxy contract + constantOutflowNFTProxy.initializeProxy(address(constantOutflowNFTLogic)); + + // Initialize CIFNFT proxy contract + constantInflowNFTProxy.initializeProxy(address(constantInflowNFTLogic)); + + // Initialize Pool Admin NFT proxy contract + poolAdminNFTProxy.initializeProxy(address(poolAdminNFTLogic)); + + // Initialize Pool Member NFT proxy contract + poolMemberNFTProxy.initializeProxy(address(poolMemberNFTLogic)); + + // // Initialize COFNFT proxy contract + IConstantOutflowNFT(address(constantOutflowNFTProxy)).initialize("Constant Outflow NFT", "COF"); + + // // Initialize CIFNFT proxy contract + IConstantInflowNFT(address(constantInflowNFTProxy)).initialize("Constant Inflow NFT", "CIF"); + + // // Initialize Pool Admin NFT proxy contract + IPoolAdminNFT(address(poolAdminNFTProxy)).initialize("Pool Admin NFT", "PA"); + + // // Initialize Pool Member NFT proxy contract + IPoolMemberNFT(address(poolMemberNFTProxy)).initialize("Pool Member NFT", "PM"); + + constantOutflowNFT = ConstantOutflowNFT(address(constantOutflowNFTProxy)); + constantInflowNFT = ConstantInflowNFT(address(constantInflowNFTProxy)); + poolAdminNFT = PoolAdminNFT(address(poolAdminNFTProxy)); + poolMemberNFT = PoolMemberNFT(address(poolMemberNFTProxy)); } else if (step == 6) { + // PERIPHERAL CONTRACTS: SuperToken Logic and SuperTokenFactory Logic // Deploy SuperToken Logic // Deploy SuperToken Factory - _deploySuperTokenLogicAndSuperTokenFactoryAndUpdateContracts(); + + // _deploySuperTokenLogic(); + // Deploy canonical SuperToken logic contract + superTokenLogic = SuperToken( + SuperTokenDeployerLibrary.deploySuperTokenLogic( + host, + IConstantOutflowNFT(address(constantOutflowNFT)), + IConstantInflowNFT(address(constantInflowNFT)), + IPoolAdminNFT(address(poolAdminNFT)), + IPoolMemberNFT(address(poolMemberNFT)) + ) + ); + + // _deploySuperTokenFactory(); + superTokenFactoryLogic = SuperfluidPeripheryDeployerLibrary.deploySuperTokenFactory( + host, + superTokenLogic, + constantOutflowNFTLogic, + constantInflowNFTLogic, + poolAdminNFTLogic, + poolMemberNFTLogic + ); + + // _setSuperTokenFactoryInHost(); + // 'Update' code with Governance and register SuperTokenFactory with Superfluid + testGovernance.updateContracts( + host, address(0), new address[](0), address(superTokenFactoryLogic), address(0) + ); + + // we set the canonical address based on host.getSuperTokenFactory() because + // in the upgradeable case, we create a new proxy contract in the function + // and set it as the canonical supertokenfactory. + superTokenFactory = SuperTokenFactory(address(host.getSuperTokenFactory())); } else if (step == 7) { + // PERIPHERAL CONTRACTS: Resolver, SuperfluidLoader, TOGA, BatchLiquidator // Deploy TestResolver - // Deploy SuperfluidLoader and make SuperfluidFrameworkDpeloyer an admin for the TestResolver + // Deploy SuperfluidLoader and make SuperfluidFrameworkDeployer an admin for the TestResolver // Set TestGovernance, Superfluid, SuperfluidLoader and CFAv1Forwarder in TestResolver - _deployTestResolverAndSuperfluidLoaderAndSet(address(this)); + + // _deployTestResolver(resolverAdmin); + if (address(host) == address(0)) revert DEPLOY_PERIPHERALS_REQUIRES_DEPLOY_CORE(); + testResolver = SuperfluidPeripheryDeployerLibrary.deployTestResolver(address(this)); + + // _deploySuperfluidLoader(); + superfluidLoader = SuperfluidLoaderDeployerLibrary.deploySuperfluidLoader(testResolver); + + // _setAddressesInResolver(); + // Register Governance with Resolver + testResolver.set("TestGovernance.test", address(testGovernance)); + + // Register Superfluid with Resolver + testResolver.set("Superfluid.test", address(host)); + + // Register SuperfluidLoader with Resolver + testResolver.set("SuperfluidLoader-v1", address(superfluidLoader)); + + // Register CFAv1Forwarder with Resolver + testResolver.set("CFAv1Forwarder", address(cfaV1Forwarder)); + + // Register IDAv1Forwarder with Resolver + testResolver.set("IDAv1Forwarder", address(idaV1Forwarder)); + + // Register GDAv1Forwarder with Resolver + testResolver.set("GDAv1Forwarder", address(gdaV1Forwarder)); + // Make SuperfluidFrameworkDeployer deployer an admin for the TestResolver as well testResolver.addAdmin(msg.sender); + + // _deployTOGA(); + if (!_is1820Deployed()) revert DEPLOY_TOGA_REQUIRES_1820(); + toga = new TOGA(host, DEFAULT_TOGA_MIN_BOND_DURATION); + testGovernance.setRewardAddress(host, ISuperfluidToken(address(0)), address(toga)); + + // _deployBatchLiquidator(); + if (address(cfaV1) == address(0)) revert DEPLOY_PERIPHERALS_REQUIRES_DEPLOY_CORE(); + if (address(cfaV1) == address(0)) revert DEPLOY_PERIPHERALS_REQUIRES_DEPLOY_AGREEMENTS(); + batchLiquidator = new BatchLiquidator(address(host)); } else { revert("Invalid step"); } currentStep++; } + + function _is1820Deployed() internal view returns (bool) { + uint256 codeSize; + assembly { + codeSize := extcodesize(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24) + } + return codeSize != 0; + } } //// External Libraries //// @@ -430,6 +468,22 @@ library SuperfluidIDAv1DeployerLibrary { } } +/// @title SuperfluidGDAv1DeployerLibrary +/// @author Superfluid +/// @notice An external library that deploys Superfluid GeneralDistributionAgreementV1 contract +/// @dev This library is used for testing purposes only, not deployments to test OR production networks +library SuperfluidGDAv1DeployerLibrary { + /// @notice deploys the Superfluid GeneralDistributionAgreementV1 Contract + /// @param _host Superfluid host address + /// @return newly deployed GeneralDistributionAgreementV1 contract + function deployGeneralDistributionAgreementV1(ISuperfluid _host) + external + returns (GeneralDistributionAgreementV1) + { + return new GeneralDistributionAgreementV1(_host); + } +} + /// @title SuperfluidCFAv1DeployerLibrary /// @author Superfluid /// @notice An external library that deploys Superfluid ConstantFlowAgreementV1 contract @@ -449,12 +503,19 @@ library SuperfluidCFAv1DeployerLibrary { library SuperTokenDeployerLibrary { /// @notice Deploy a SuperToken logic contract /// @param host the address of the host contract + /// @param constantOutflowNFT the address of the ConstantOutflowNFT contract + /// @param constantInflowNFT the address of the ConstantInflowNFT contract + /// @param poolAdminNFT the address of the PoolAdminNFT contract + /// @param poolMemberNFT the address of the PoolMemberNFT contract + /// @return the address of the newly deployed SuperToken logic contract function deploySuperTokenLogic( ISuperfluid host, IConstantOutflowNFT constantOutflowNFT, - IConstantInflowNFT constantInflowNFT + IConstantInflowNFT constantInflowNFT, + IPoolAdminNFT poolAdminNFT, + IPoolMemberNFT poolMemberNFT ) external returns (address) { - return address(new SuperToken(host, constantOutflowNFT, constantInflowNFT)); + return address(new SuperToken(host, constantOutflowNFT, constantInflowNFT, poolAdminNFT, poolMemberNFT)); } } @@ -466,18 +527,26 @@ library SuperfluidPeripheryDeployerLibrary { /// @dev deploys Super Token Factory contract /// @param _host address of the Superfluid contract /// @param _superTokenLogic address of the Super Token logic contract + /// @param constantOutflowNFTLogic address of the Constant Outflow NFT logic contract + /// @param constantInflowNFTLogic address of the Constant Inflow NFT logic contract + /// @param poolAdminNFTLogic address of the Pool Admin NFT logic contract + /// @param poolMemberNFTLogic address of the Pool Member NFT logic contract /// @return newly deployed SuperTokenFactory contract function deploySuperTokenFactory( ISuperfluid _host, ISuperToken _superTokenLogic, - IConstantOutflowNFT constantOutflowNFT, - IConstantInflowNFT constantInflowNFT + IConstantOutflowNFT constantOutflowNFTLogic, + IConstantInflowNFT constantInflowNFTLogic, + IPoolAdminNFT poolAdminNFTLogic, + IPoolMemberNFT poolMemberNFTLogic ) external returns (SuperTokenFactory) { return new SuperTokenFactory( _host, _superTokenLogic, - constantOutflowNFT, - constantInflowNFT + constantOutflowNFTLogic, + constantInflowNFTLogic, + poolAdminNFTLogic, + poolMemberNFTLogic ); } @@ -507,6 +576,15 @@ library IDAv1ForwarderDeployerLibrary { } } +library GDAv1ForwarderDeployerLibrary { + /// @notice deploys the Superfluid GDAv1Forwarder contract + /// @param _host Superfluid host address + /// @return newly deployed GDAv1Forwarder contract + function deployGDAv1Forwarder(ISuperfluid _host) external returns (GDAv1Forwarder) { + return new GDAv1Forwarder(_host); + } +} + library SuperfluidLoaderDeployerLibrary { /// @notice deploys the Superfluid SuperfluidLoader contract /// @param _resolver Superfluid resolver address @@ -516,7 +594,15 @@ library SuperfluidLoaderDeployerLibrary { } } -library SuperfluidNFTLogicDeployerLibrary { +library SuperfluidPoolLogicDeployerLibrary { + /// @notice deploys the Superfluid SuperfluidPool contract + /// @return newly deployed SuperfluidPool contract + function deploySuperfluidPool(GeneralDistributionAgreementV1 _gda) external returns (SuperfluidPool) { + return new SuperfluidPool(_gda); + } +} + +library SuperfluidFlowNFTLogicDeployerLibrary { /// @notice deploys the Superfluid ConstantOutflowNFT contract /// @param _host Superfluid host address /// @param _constantInflowNFTProxy address of the ConstantInflowNFT proxy contract @@ -540,10 +626,30 @@ library SuperfluidNFTLogicDeployerLibrary { } } +library SuperfluidPoolNFTLogicDeployerLibrary { + /// @notice deploys the Superfluid PoolAdminNFT contract + /// @param _host Superfluid host address + /// @return newly deployed PoolAdminNFT contract + function deployPoolAdminNFT(ISuperfluid _host) external returns (PoolAdminNFT) { + return new PoolAdminNFT(_host); + } + + /// @notice deploys the Superfluid PoolMemberNFT contract + /// @param _host Superfluid host address + /// @return newly deployed PoolMemberNFT contract + function deployPoolMemberNFT(ISuperfluid _host) external returns (PoolMemberNFT) { + return new PoolMemberNFT(_host); + } +} + library ProxyDeployerLibrary { function deployUUPSProxy() external returns (UUPSProxy) { return new UUPSProxy(); } + + function deploySuperfluidUpgradeableBeacon(address logicContract) external returns (SuperfluidUpgradeableBeacon) { + return new SuperfluidUpgradeableBeacon(logicContract); + } } library TokenDeployerLibrary { diff --git a/packages/ethereum-contracts/contracts/utils/SuperfluidLoader.sol b/packages/ethereum-contracts/contracts/utils/SuperfluidLoader.sol index 1051b919ee..9776250b83 100644 --- a/packages/ethereum-contracts/contracts/utils/SuperfluidLoader.sol +++ b/packages/ethereum-contracts/contracts/utils/SuperfluidLoader.sol @@ -26,6 +26,7 @@ contract SuperfluidLoader { ISuperTokenFactory superTokenFactory; ISuperAgreement agreementCFAv1; ISuperAgreement agreementIDAv1; + ISuperAgreement agreementGDAv1; } constructor(IResolver resolver) { @@ -51,5 +52,8 @@ contract SuperfluidLoader { result.agreementIDAv1 = result.superfluid.getAgreementClass( keccak256("org.superfluid-finance.agreements.InstantDistributionAgreement.v1") ); + result.agreementGDAv1 = result.superfluid.getAgreementClass( + keccak256("org.superfluid-finance.agreements.GeneralDistributionAgreement.v1") + ); } } diff --git a/packages/ethereum-contracts/dev-scripts/deploy-contracts-and-token.js b/packages/ethereum-contracts/dev-scripts/deploy-contracts-and-token.js index a350f665eb..34bf331116 100644 --- a/packages/ethereum-contracts/dev-scripts/deploy-contracts-and-token.js +++ b/packages/ethereum-contracts/dev-scripts/deploy-contracts-and-token.js @@ -19,11 +19,12 @@ async function deployContractsAndToken() { await deployer .connect(Deployer) - ["deployWrapperSuperToken(string,string,uint8,uint256)"]( + .deployWrapperSuperToken( "Fake DAI", "fDAI", 18, - ethers.utils.parseUnits("1000000000000") + ethers.utils.parseUnits("1000000000000"), + ethers.constants.AddressZero ); await deployer diff --git a/packages/ethereum-contracts/dev-scripts/deploy-test-framework.js b/packages/ethereum-contracts/dev-scripts/deploy-test-framework.js index 348b5b631a..7f07237592 100644 --- a/packages/ethereum-contracts/dev-scripts/deploy-test-framework.js +++ b/packages/ethereum-contracts/dev-scripts/deploy-test-framework.js @@ -1,16 +1,21 @@ const {ethers} = require("hardhat"); const {JsonRpcProvider} = require("@ethersproject/providers"); +const SuperfluidPoolDeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol/SuperfluidPoolDeployerLibrary.json"); const SuperfluidGovDeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/SuperfluidGovDeployerLibrary.json"); const SuperfluidHostDeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/SuperfluidHostDeployerLibrary.json"); const SuperfluidCFAv1DeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/SuperfluidCFAv1DeployerLibrary.json"); const SuperfluidIDAv1DeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/SuperfluidIDAv1DeployerLibrary.json"); +const SuperfluidGDAv1DeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/SuperfluidGDAv1DeployerLibrary.json"); const SuperTokenDeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/SuperTokenDeployerLibrary.json"); const SuperfluidPeripheryDeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/SuperfluidPeripheryDeployerLibrary.json"); -const SuperfluidNFTLogicDeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/SuperfluidNFTLogicDeployerLibrary.json"); +const SuperfluidPoolLogicDeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/SuperfluidPoolLogicDeployerLibrary.json"); +const SuperfluidFlowNFTLogicDeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/SuperfluidFlowNFTLogicDeployerLibrary.json"); +const SuperfluidPoolNFTLogicDeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/SuperfluidPoolNFTLogicDeployerLibrary.json"); const ProxyDeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/ProxyDeployerLibrary.json"); const CFAv1ForwarderDeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/CFAv1ForwarderDeployerLibrary.json"); const IDAv1ForwarderDeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/IDAv1ForwarderDeployerLibrary.json"); +const GDAv1ForwarderDeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/GDAv1ForwarderDeployerLibrary.json"); const SuperfluidLoaderDeployerLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol/SuperfluidLoaderDeployerLibrary.json"); const SuperfluidFrameworkDeployerArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/SuperfluidFrameworkDeployer.sol/SuperfluidFrameworkDeployer.json"); const SlotsBitmapLibraryArtifact = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/libs/SlotsBitmapLibrary.sol/SlotsBitmapLibrary.json"); @@ -145,6 +150,27 @@ const _deployTestFramework = async (provider, signer) => { }, } ); + + const SuperfluidPoolDeployerLibrary = + await _getFactoryAndReturnDeployedContract( + "SuperfluidPoolDeployerLibrary", + SuperfluidPoolDeployerLibraryArtifact, + signer + ); + + const SuperfluidGDAv1DeployerLibrary = + await _getFactoryAndReturnDeployedContract( + "SuperfluidGDAv1DeployerLibrary", + SuperfluidGDAv1DeployerLibraryArtifact, + { + signer, + libraries: { + SuperfluidPoolDeployerLibrary: + SuperfluidPoolDeployerLibrary.address, + SlotsBitmapLibrary: SlotsBitmapLibrary.address, + }, + } + ); const SuperTokenDeployerLibrary = await _getFactoryAndReturnDeployedContract( "SuperTokenDeployerLibrary", @@ -159,10 +185,23 @@ const _deployTestFramework = async (provider, signer) => { SuperfluidPeripheryDeployerLibraryArtifact, signer ); - const SuperfluidNFTLogicDeployerLibrary = + + const SuperfluidPoolLogicDeployerLibrary = + await _getFactoryAndReturnDeployedContract( + "SuperfluidPoolLogicDeployerLibrary", + SuperfluidPoolLogicDeployerLibraryArtifact, + signer + ); + const SuperfluidFlowNFTLogicDeployerLibrary = await _getFactoryAndReturnDeployedContract( - "SuperfluidNFTLogicDeployerLibrary", - SuperfluidNFTLogicDeployerLibraryArtifact, + "SuperfluidFlowNFTLogicDeployerLibrary", + SuperfluidFlowNFTLogicDeployerLibraryArtifact, + signer + ); + const SuperfluidPoolNFTLogicDeployerLibrary = + await _getFactoryAndReturnDeployedContract( + "SuperfluidPoolNFTLogicDeployerLibrary", + SuperfluidPoolNFTLogicDeployerLibraryArtifact, signer ); const ProxyDeployerLibrary = await _getFactoryAndReturnDeployedContract( @@ -182,6 +221,12 @@ const _deployTestFramework = async (provider, signer) => { IDAv1ForwarderDeployerLibraryArtifact, signer ); + const GDAv1ForwarderDeployerLibrary = + await _getFactoryAndReturnDeployedContract( + "GDAv1ForwarderDeployerLibrary", + GDAv1ForwarderDeployerLibraryArtifact, + signer + ); const SuperfluidLoaderDeployerLibrary = await _getFactoryAndReturnDeployedContract( "SuperfluidLoaderDeployerLibrary", @@ -212,14 +257,23 @@ const _deployTestFramework = async (provider, signer) => { SuperfluidIDAv1DeployerLibrary: getContractAddress( SuperfluidIDAv1DeployerLibrary ), + SuperfluidGDAv1DeployerLibrary: getContractAddress( + SuperfluidGDAv1DeployerLibrary + ), SuperfluidPeripheryDeployerLibrary: getContractAddress( SuperfluidPeripheryDeployerLibrary ), SuperTokenDeployerLibrary: getContractAddress( SuperTokenDeployerLibrary ), - SuperfluidNFTLogicDeployerLibrary: getContractAddress( - SuperfluidNFTLogicDeployerLibrary + SuperfluidPoolLogicDeployerLibrary: getContractAddress( + SuperfluidPoolLogicDeployerLibrary + ), + SuperfluidFlowNFTLogicDeployerLibrary: getContractAddress( + SuperfluidFlowNFTLogicDeployerLibrary + ), + SuperfluidPoolNFTLogicDeployerLibrary: getContractAddress( + SuperfluidPoolNFTLogicDeployerLibrary ), ProxyDeployerLibrary: getContractAddress(ProxyDeployerLibrary), CFAv1ForwarderDeployerLibrary: getContractAddress( @@ -228,6 +282,9 @@ const _deployTestFramework = async (provider, signer) => { IDAv1ForwarderDeployerLibrary: getContractAddress( IDAv1ForwarderDeployerLibrary ), + GDAv1ForwarderDeployerLibrary: getContractAddress( + GDAv1ForwarderDeployerLibrary + ), SuperfluidLoaderDeployerLibrary: getContractAddress( SuperfluidLoaderDeployerLibrary ), diff --git a/packages/ethereum-contracts/dev-scripts/run-deploy-contracts-and-token.js b/packages/ethereum-contracts/dev-scripts/run-deploy-contracts-and-token.js index 3cd8f0505e..e81ebc157e 100644 --- a/packages/ethereum-contracts/dev-scripts/run-deploy-contracts-and-token.js +++ b/packages/ethereum-contracts/dev-scripts/run-deploy-contracts-and-token.js @@ -12,6 +12,7 @@ deployContractsAndToken() hostAddress: frameworkAddresses.host, cfaAddress: frameworkAddresses.cfa, idaAddress: frameworkAddresses.ida, + gdaAddress: frameworkAddresses.gda, superTokenFactoryAddress: frameworkAddresses.superTokenFactory, resolverV1Address: frameworkAddresses.resolver, nativeAssetSuperTokenAddress: @@ -44,4 +45,4 @@ deployContractsAndToken() .catch((err) => { console.error(err); process.exit(1); - }); + }); \ No newline at end of file diff --git a/packages/ethereum-contracts/foundry.toml b/packages/ethereum-contracts/foundry.toml index 36101b5039..04e9527012 100644 --- a/packages/ethereum-contracts/foundry.toml +++ b/packages/ethereum-contracts/foundry.toml @@ -9,6 +9,7 @@ ignored_error_codes = [5159] # selfdestruct in contracts/mocks/SuperfluidDestruc evm_version = 'paris' remappings = [ '@superfluid-finance/ethereum-contracts/contracts/=packages/ethereum-contracts/contracts/', + '@superfluid-finance/solidity-semantic-money/src/=packages/solidity-semantic-money/src/', '@openzeppelin/=node_modules/@openzeppelin/', 'ds-test/=lib/forge-std/lib/ds-test/src/', 'forge-std/=lib/forge-std/src/'] diff --git a/packages/ethereum-contracts/ops-scripts/deploy-aux-contracts.js b/packages/ethereum-contracts/ops-scripts/deploy-aux-contracts.js index 0f12471c79..41c68afb2c 100644 --- a/packages/ethereum-contracts/ops-scripts/deploy-aux-contracts.js +++ b/packages/ethereum-contracts/ops-scripts/deploy-aux-contracts.js @@ -80,10 +80,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( console.log("deploying solvency related contracts"); const minBondDuration = process.env.TOGA_MIN_BOND_DURATION || 604800; - const toga = await TOGA.new( - sf.host.address, - minBondDuration - ); + const toga = await TOGA.new(sf.host.address, minBondDuration); console.log("TOGA deployed at:", toga.address); await gov.setRewardAddress( @@ -93,10 +90,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( ); console.log("reward address set to TOGA"); - const batchLiquidator = await BatchLiquidator.new( - sf.host.address, - sf.agreements.cfa.address, - ); + const batchLiquidator = await BatchLiquidator.new(sf.host.address); console.log("BatchLiquidator deployed at:", batchLiquidator.address); await oldGov.replaceGovernance(sf.host.address, govProxy.address); diff --git a/packages/ethereum-contracts/ops-scripts/deploy-deterministically.js b/packages/ethereum-contracts/ops-scripts/deploy-deterministically.js index 59127e6ae4..9b1da6c5bc 100644 --- a/packages/ethereum-contracts/ops-scripts/deploy-deterministically.js +++ b/packages/ethereum-contracts/ops-scripts/deploy-deterministically.js @@ -5,6 +5,7 @@ const Resolver = artifacts.require("Resolver"); const SuperfluidLoader = artifacts.require("SuperfluidLoader"); const CFAv1Forwarder = artifacts.require("CFAv1Forwarder"); +const GDAv1Forwarder = artifacts.require("GDAv1Forwarder"); /** * @dev Deploy specified contract at a deterministic address (defined by sender, nonce) @@ -80,6 +81,12 @@ module.exports = eval(`(${S.toString()})()`)(async function ( console.log( `setting up CFAv1Forwarder for chainId ${chainId}, host ${hostAddr}` ); + } else if (contractName === "GDAv1Forwarder") { + ContractArtifact = GDAv1Forwarder; + deployArgs = [hostAddr]; + console.log( + `setting up GDAv1Forwarder for chainId ${chainId}, host ${hostAddr}` + ); } else { throw new Error("Contract unknown / not supported"); } @@ -165,5 +172,7 @@ module.exports = eval(`(${S.toString()})()`)(async function ( const deployTxReceipt = await web3.eth.sendSignedTransaction( signedTx.rawTransaction ); - console.log("contract deployed at:", deployTxReceipt.contractAddress); + // make it easy to get the deployed address with `tail -n 1` + console.log("contract deployed at:"); + console.log(deployTxReceipt.contractAddress); }); diff --git a/packages/ethereum-contracts/ops-scripts/deploy-framework.js b/packages/ethereum-contracts/ops-scripts/deploy-framework.js index 9f04525a41..e41c8d4e2a 100644 --- a/packages/ethereum-contracts/ops-scripts/deploy-framework.js +++ b/packages/ethereum-contracts/ops-scripts/deploy-framework.js @@ -102,6 +102,8 @@ async function deployContractIfCodeChanged( * (overriding env: RELEASE_VERSION) * @param {string} options.outputFile Name of file where to log addresses of newly deployed contracts * (overriding env: OUTPUT_FILE) + * @param {boolean} options.newSuperfluidLoader Deploy a new superfluid loader contract + * (overriding env: NEW_SUPERFLUID_LOADER) * * Usage: npx truffle exec ops-scripts/deploy-framework.js */ @@ -118,6 +120,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( appWhiteListing, protocolReleaseVersion, outputFile, + newSuperfluidLoader, } = options; resetSuperfluidFramework = options.resetSuperfluidFramework; @@ -159,6 +162,9 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( const IDAv1_TYPE = web3.utils.sha3( "org.superfluid-finance.agreements.InstantDistributionAgreement.v1" ); + const GDAv1_TYPE = web3.utils.sha3( + "org.superfluid-finance.agreements.GeneralDistributionAgreement.v1" + ); newTestResolver = newTestResolver || !!process.env.CREATE_NEW_RESOLVER; useMocks = useMocks || !!process.env.USE_MOCKS; @@ -167,6 +173,8 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( appWhiteListing || config.gov_enableAppWhiteListing || !!process.env.ENABLE_APP_WHITELISTING; + newSuperfluidLoader = newSuperfluidLoader || !!process.env.NEW_SUPERFLUID_LOADER; + console.log("app whitelisting enabled:", appWhiteListing); if (newTestResolver) { console.log("**** !ATTN! CREATING NEW RESOLVER ****"); @@ -177,8 +185,8 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( if (nonUpgradable) { console.log("**** !ATTN! DISABLED UPGRADABILITY ****"); } - if (appWhiteListing) { - console.log("**** !ATTN! ENABLING APP WHITELISTING ****"); + if (newSuperfluidLoader) { + console.log("**** !ATTN! DEPLOYING NEW SUPERFLUID LOADER ****"); } await deployERC1820((err) => { @@ -188,6 +196,8 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( const contracts = [ "Ownable", "CFAv1Forwarder", + "IDAv1Forwarder", + "GDAv1Forwarder", "IMultiSigWallet", "ISafe", "SuperfluidGovernanceBase", @@ -203,8 +213,14 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( "SlotsBitmapLibrary", "ConstantFlowAgreementV1", "InstantDistributionAgreementV1", + "GeneralDistributionAgreementV1", + "SuperfluidUpgradeableBeacon", + "SuperfluidPool", + "SuperfluidPoolDeployerLibrary", "ConstantOutflowNFT", "ConstantInflowNFT", + "PoolAdminNFT", + "PoolMemberNFT", "IAccessControlEnumerable", ]; const mockContracts = [ @@ -217,6 +233,8 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( IMultiSigWallet, ISafe, CFAv1Forwarder, + IDAv1Forwarder, + GDAv1Forwarder, SuperfluidGovernanceBase, Resolver, SuperfluidLoader, @@ -233,8 +251,14 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( SlotsBitmapLibrary, ConstantFlowAgreementV1, InstantDistributionAgreementV1, + GeneralDistributionAgreementV1, + SuperfluidUpgradeableBeacon, + SuperfluidPool, + SuperfluidPoolDeployerLibrary, ConstantOutflowNFT, ConstantInflowNFT, + PoolAdminNFT, + PoolMemberNFT, IAccessControlEnumerable, } = await SuperfluidSDK.loadContracts({ ...extractWeb3Options(options), @@ -283,7 +307,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( await deployAndRegisterContractIf( SuperfluidLoader, "SuperfluidLoader-v1", - async (contractAddress) => contractAddress === ZERO_ADDRESS, + async (contractAddress) => newSuperfluidLoader === true || contractAddress === ZERO_ADDRESS, async () => { const c = await web3tx( SuperfluidLoader.new, @@ -348,7 +372,9 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( ); // this is needed later on const superfluidConstructorParam = superfluid.address - .toLowerCase().slice(2).padStart(64, "0"); + .toLowerCase() + .slice(2) + .padStart(64, "0"); // load existing governance if needed if (!governance) { @@ -368,6 +394,9 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( if (config.cfaFwd) { trustedForwarders.push(config.cfaFwd); } + if (config.gdaFwd) { + trustedForwarders.push(config.gdaFwd); + } console.log(`initializing governance with config: ${JSON.stringify({ liquidationPeriod: config.liquidationPeriod, patricianPeriod: config.patricityPeriod, @@ -449,16 +478,18 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( return externalLibrary; }; + let slotsBitmapLibraryAddress = ZERO_ADDRESS; // list IDA v1 const deployIDAv1 = async () => { // small inefficiency: this may be re-deployed even if not changed // deploySlotsBitmapLibrary - await deployExternalLibraryAndLink( + const slotsBitmapLibrary = await deployExternalLibraryAndLink( SlotsBitmapLibrary, "SlotsBitmapLibrary", - "SLOTS_BITMAP_LIBRARY_ADDRESS", + "SLOTS_BITMAP_LIBRARY", InstantDistributionAgreementV1 ); + slotsBitmapLibraryAddress = slotsBitmapLibrary.address; const agreement = await web3tx( InstantDistributionAgreementV1.new, "InstantDistributionAgreementV1.new" @@ -482,7 +513,6 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( // here as an optimization, this assumes that we do not change the // library code. // link library in order to avoid spurious code change detections - let slotsBitmapLibraryAddress = ZERO_ADDRESS; try { const IDAv1 = await InstantDistributionAgreementV1.at( await superfluid.getAgreementClass.call(IDAv1_TYPE) @@ -507,6 +537,94 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( } } + // @note GDA deployment is commented out until we plan on releasing it + const deployGDAv1 = async () => { + try { + // deploy and link SuperfluidPoolDeployerLibrary + await deployExternalLibraryAndLink( + SuperfluidPoolDeployerLibrary, + "SuperfluidPoolDeployerLibrary", + "SUPERFLUID_POOL_DEPLOYER", + GeneralDistributionAgreementV1 + ); + + if (process.env.IS_HARDHAT) { + if (slotsBitmapLibraryAddress !== ZERO_ADDRESS) { + const lib = await SlotsBitmapLibrary.at( + slotsBitmapLibraryAddress + ); + GeneralDistributionAgreementV1.link(lib); + } + } else { + GeneralDistributionAgreementV1.link( + "SlotsBitmapLibrary", + slotsBitmapLibraryAddress + ); + } + } catch (err) { + console.error(err); + } + const agreement = await web3tx( + GeneralDistributionAgreementV1.new, + "GeneralDistributionAgreementV1.new" + )(superfluid.address); + + console.log( + "New GeneralDistributionAgreementV1 address", + agreement.address + ); + output += `GDA_LOGIC=${agreement.address}\n`; + return agreement; + }; + + if (!(await superfluid.isAgreementTypeListed.call(GDAv1_TYPE))) { + const gda = await deployGDAv1(); + await web3tx( + governance.registerAgreementClass, + "Governance registers GDA" + )(superfluid.address, gda.address); + } else { + // NOTE that we are reusing the existing deployed external library + // here as an optimization, this assumes that we do not change the + // library code. + // link library in order to avoid spurious code change detections + try { + const GDAv1 = await GeneralDistributionAgreementV1.at( + await superfluid.getAgreementClass.call(GDAv1_TYPE) + ); + slotsBitmapLibraryAddress = + await GDAv1.SLOTS_BITMAP_LIBRARY_ADDRESS.call(); + let superfluidPoolDeployerLibraryAddress = + await GDAv1.SUPERFLUID_POOL_DEPLOYER_ADDRESS.call(); + if (process.env.IS_HARDHAT) { + if (slotsBitmapLibraryAddress !== ZERO_ADDRESS) { + const lib = await SlotsBitmapLibrary.at( + slotsBitmapLibraryAddress + ); + GeneralDistributionAgreementV1.link(lib); + } + if (superfluidPoolDeployerLibraryAddress !== ZERO_ADDRESS) { + const lib = await SuperfluidPoolDeployerLibrary.at( + superfluidPoolDeployerLibraryAddress + ); + GeneralDistributionAgreementV1.link(lib); + } + } else { + GeneralDistributionAgreementV1.link( + "SlotsBitmapLibrary", + slotsBitmapLibraryAddress + ); + GeneralDistributionAgreementV1.link( + "SuperfluidPoolDeployerLibrary", + superfluidPoolDeployerLibraryAddress + ); + } + } catch (e) { + console.warn("Cannot get slotsBitmapLibrary address", e.toString()); + } + } + // @note GDA deployment is commented out until we plan on releasing it + if (protocolReleaseVersion === "test") { // deploy CFAv1Forwarder for test deployments // for other (permanent) deployments, it's not handled by this script @@ -519,7 +637,41 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( output += `CFA_V1_FORWARDER=${forwarder.address}\n`; await web3tx( governance.enableTrustedForwarder, - "Governance set CFAv1Forwarder" + `Governance set CFAv1Forwarder` + )(superfluid.address, ZERO_ADDRESS, forwarder.address); + return forwarder; + } + ); + + // deploy IDAv1Forwarder for test deployments + // for other (permanent) deployments, it's not handled by this script + await deployAndRegisterContractIf( + IDAv1Forwarder, + "IDAv1Forwarder", + async (contractAddress) => contractAddress === ZERO_ADDRESS, + async () => { + const forwarder = await IDAv1Forwarder.new(superfluid.address); + output += `IDA_V1_FORWARDER=${forwarder.address}\n`; + await web3tx( + governance.enableTrustedForwarder, + `Governance set IDAv1Forwarder` + )(superfluid.address, ZERO_ADDRESS, forwarder.address); + return forwarder; + } + ); + + // deploy GDAv1Forwarder for test deployments + // for other (permanent) deployments, it's not handled by this script + await deployAndRegisterContractIf( + GDAv1Forwarder, + "GDAv1Forwarder", + async (contractAddress) => contractAddress === ZERO_ADDRESS, + async () => { + const forwarder = await GDAv1Forwarder.new(superfluid.address); + output += `GDA_V1_FORWARDER=${forwarder.address}\n`; + await web3tx( + governance.enableTrustedForwarder, + `Governance set GDAv1Forwarder` )(superfluid.address, ZERO_ADDRESS, forwarder.address); return forwarder; } @@ -567,9 +719,9 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( ZERO_ADDRESS.toLowerCase().slice(2).padStart(64, "0"), ] ); - if (cfaNewLogicAddress !== ZERO_ADDRESS) + if (cfaNewLogicAddress !== ZERO_ADDRESS) { agreementsToUpdate.push(cfaNewLogicAddress); - + } // deploy new IDA logic const idaNewLogicAddress = await deployContractIfCodeChanged( web3, @@ -585,8 +737,28 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( superfluidConstructorParam, ] ); - if (idaNewLogicAddress !== ZERO_ADDRESS) + if (idaNewLogicAddress !== ZERO_ADDRESS) { agreementsToUpdate.push(idaNewLogicAddress); + } + // @note commented out: deploy new GDA logic + const gdaNewLogicAddress = await deployContractIfCodeChanged( + web3, + GeneralDistributionAgreementV1, + await ( + await UUPSProxiable.at( + await superfluid.getAgreementClass.call(GDAv1_TYPE) + ) + ).getCodeAddress(), + async () => (await deployGDAv1()).address, + [ + // See SuperToken constructor parameter + superfluidConstructorParam, + ] + ); + if (gdaNewLogicAddress !== ZERO_ADDRESS) { + agreementsToUpdate.push(gdaNewLogicAddress); + } + // @note GDA deployment is commented out until we plan on releasing it } // deploy new super token factory logic (depends on SuperToken logic, which links to nft deployer library) @@ -600,23 +772,17 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( let constantOutflowNFTLogicChanged = false; let constantInflowNFTLogicChanged = false; + let poolAdminNFTLogicChanged = false; + let poolMemberNFTLogicChanged = false; + + const deployNFTContract = async (artifact, nftType, nftTypeCaps, args) => { + const nftLogic = await web3tx(artifact.new, `${nftType}.new`)(...args); + console.log(`${nftType} Logic address`, nftLogic.address); + output += `${nftTypeCaps}=${nftLogic.address}\n`; - const deployNFTContract = async (isOutflow, args) => { - const artifact = isOutflow ? ConstantOutflowNFT : ConstantInflowNFT; - const nftType = isOutflow ? "ConstantOutflowNFT" : "ConstantInflowNFT"; - const nftTypeCaps = isOutflow - ? "CONSTANT_OUTFLOW_NFT" - : "CONSTANT_INFLOW_NFT"; - const flowNFTLogic = await web3tx( - artifact.new, - `${nftType}.new` - )(...args); - console.log(`${nftType} Logic address`, flowNFTLogic.address); - output += `${nftTypeCaps}=${flowNFTLogic.address}\n`; - - await flowNFTLogic.castrate(); - - return flowNFTLogic; + await nftLogic.castrate(); + + return nftLogic; }; const superTokenFactoryNewLogicAddress = await deployContractIf( @@ -643,13 +809,21 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( const cofNFTLAddr = await cofNFTContract.getCodeAddress(); const cifNFTLAddr = await cifNFTContract.getCodeAddress(); + const poolAdminNFTPAddr = await superTokenLogic.POOL_ADMIN_NFT(); + const poolMemberNFTPAddr = await superTokenLogic.POOL_MEMBER_NFT(); + const poolAdminNFTContract = await PoolAdminNFT.at(poolAdminNFTPAddr); + const poolMemberNFTContract = await PoolMemberNFT.at(poolMemberNFTPAddr); + const poolAdminNFTLAddr = await poolAdminNFTContract.getCodeAddress(); + const poolMemberNFTLAddr = await poolMemberNFTContract.getCodeAddress(); + const cfaPAddr = await superfluid.getAgreementClass.call(CFAv1_TYPE); + const gdaPAddr = await superfluid.getAgreementClass.call(GDAv1_TYPE); constantOutflowNFTLogicChanged = await codeChanged( web3, ConstantOutflowNFT, cofNFTLAddr, - [superfluidConstructorParam, ap(cifNFTPAddr), ap(cfaPAddr)] + [superfluidConstructorParam, ap(cifNFTPAddr), ap(cfaPAddr), ap(gdaPAddr)] ); console.log(" constantOutflowNFTLogicChanged:", constantOutflowNFTLogicChanged); @@ -657,15 +831,32 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( web3, ConstantInflowNFT, cifNFTLAddr, - [superfluidConstructorParam, ap(cofNFTPAddr), ap(cfaPAddr)] + [superfluidConstructorParam, ap(cofNFTPAddr), ap(cfaPAddr), ap(gdaPAddr)] ); console.log(" constantInflowNFTLogicChanged:", constantInflowNFTLogicChanged); + poolAdminNFTLogicChanged = await codeChanged( + web3, + PoolAdminNFT, + poolAdminNFTLAddr, + [superfluidConstructorParam, ap(gdaPAddr)] + ); + console.log(" poolAdminNFTLogicChanged:", poolAdminNFTLogicChanged); + + poolMemberNFTLogicChanged = await codeChanged( + web3, + PoolMemberNFT, + poolMemberNFTLAddr, + [superfluidConstructorParam, ap(gdaPAddr)] + ); + console.log(" poolMemberNFTLogicChanged:", poolMemberNFTLogicChanged); + const superTokenFactoryCodeChanged = await codeChanged( web3, SuperTokenFactoryLogic, await superfluid.getSuperTokenFactoryLogic.call(), - [superfluidConstructorParam, ap(superTokenLogicAddress), ap(cofNFTLAddr), ap(cifNFTLAddr)] + [superfluidConstructorParam, ap(superTokenLogicAddress), ap(cofNFTLAddr), ap(cifNFTLAddr), + ap(poolAdminNFTLAddr), ap(poolMemberNFTLAddr)] ); console.log(" superTokenFactoryCodeChanged:", superTokenFactoryCodeChanged); @@ -674,7 +865,10 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( SuperTokenLogic, await factory.getSuperTokenLogic.call(), // this replacement does not support SuperTokenMock - [superfluidConstructorParam, ap(cofNFTPAddr), ap(cifNFTPAddr)] + [ + superfluidConstructorParam, ap(cofNFTPAddr), ap(cifNFTPAddr), + ap(poolAdminNFTPAddr), ap(poolMemberNFTPAddr) + ] ); console.log(" superTokenLogicCodeChanged:", superTokenLogicCodeChanged); return ( @@ -685,7 +879,9 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( superTokenFactoryCodeChanged || superTokenLogicCodeChanged || constantOutflowNFTLogicChanged || - constantInflowNFTLogicChanged + constantInflowNFTLogicChanged || + poolAdminNFTLogicChanged || + poolMemberNFTLogicChanged ); } catch (e) { console.log( @@ -704,6 +900,10 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( let cifNFTProxyAddress = ZERO_ADDRESS; let cofNFTLogicAddress = ZERO_ADDRESS; let cifNFTLogicAddress = ZERO_ADDRESS; + let poolAdminNFTProxyAddress = ZERO_ADDRESS; + let poolAdminNFTLogicAddress = ZERO_ADDRESS; + let poolMemberNFTProxyAddress = ZERO_ADDRESS; + let poolMemberNFTLogicAddress = ZERO_ADDRESS; // try to get NFT proxy addresses from canonical Super Token logic if (factoryAddress !== ZERO_ADDRESS) { @@ -717,6 +917,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( const superTokenLogic = await SuperTokenLogic.at( superTokenLogicAddress ); + // Flow NFTs cofNFTProxyAddress = await superTokenLogic.CONSTANT_OUTFLOW_NFT.call(); cifNFTProxyAddress = @@ -727,6 +928,18 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( cifNFTLogicAddress = await ( await UUPSProxiable.at(cifNFTProxyAddress) ).getCodeAddress(); + + // Pool NFTs + poolAdminNFTProxyAddress = + await superTokenLogic.POOL_ADMIN_NFT.call(); + poolMemberNFTProxyAddress = + await superTokenLogic.POOL_MEMBER_NFT.call(); + poolAdminNFTLogicAddress = await ( + await UUPSProxiable.at(poolAdminNFTProxyAddress) + ).getCodeAddress(); + poolMemberNFTLogicAddress = await ( + await UUPSProxiable.at(poolMemberNFTProxyAddress) + ).getCodeAddress(); } catch (err) { console.error("Unable to get nft proxy addresses"); } @@ -735,57 +948,146 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( // if the super token logic does not have the proxies, we must deploy // new nft logic and proxies. if ( - cofNFTProxyAddress === ZERO_ADDRESS && - cifNFTProxyAddress === ZERO_ADDRESS + cofNFTProxyAddress === ZERO_ADDRESS || + cifNFTProxyAddress === ZERO_ADDRESS || + poolAdminNFTProxyAddress === ZERO_ADDRESS || + poolMemberNFTProxyAddress === ZERO_ADDRESS ) { - const constantOutflowNFTProxy = await web3tx( - UUPSProxy.new, - `Create ConstantOutflowNFT proxy` - )(); - console.log("ConstantOutflowNFT Proxy address", constantOutflowNFTProxy.address); - output += `CONSTANT_OUTFLOW_NFT_PROXY=${constantOutflowNFTProxy.address}\n`; + if ( + cofNFTProxyAddress === ZERO_ADDRESS || + cifNFTProxyAddress === ZERO_ADDRESS + ) { + const constantOutflowNFTProxy = await web3tx( + UUPSProxy.new, + `Create ConstantOutflowNFT proxy` + )(); + console.log( + "ConstantOutflowNFT Proxy address", + constantOutflowNFTProxy.address + ); + output += `CONSTANT_OUTFLOW_NFT_PROXY=${constantOutflowNFTProxy.address}\n`; + + const constantInflowNFTProxy = await web3tx( + UUPSProxy.new, + `Create ConstantInflowNFT proxy` + )(); + console.log( + "ConstantInflowNFT Proxy address", + constantInflowNFTProxy.address + ); + output += `CONSTANT_INFLOW_NFT_PROXY=${constantInflowNFTProxy.address}\n`; - const constantInflowNFTProxy = await web3tx( - UUPSProxy.new, - `Create ConstantInflowNFT proxy` - )(); - console.log("ConstantInflowNFT Proxy address", constantInflowNFTProxy.address); - output += `CONSTANT_INFLOW_NFT_PROXY=${constantInflowNFTProxy.address}\n`; + const constantOutflowNFTLogic = await deployNFTContract( + ConstantOutflowNFT, + "ConstantOutflowNFT", + "CONSTANT_OUTFLOW_NFT", + [superfluid.address, constantInflowNFTProxy.address] + ); + const constantInflowNFTLogic = await deployNFTContract( + ConstantInflowNFT, + "ConstantInflowNFT", + "CONSTANT_INFLOW_NFT", + [superfluid.address, constantOutflowNFTProxy.address] + ); - const constantOutflowNFTLogic = await deployNFTContract(true, [ - superfluid.address, - constantInflowNFTProxy.address, - ]); - const constantInflowNFTLogic = await deployNFTContract(false, [ - superfluid.address, - constantOutflowNFTProxy.address, - ]); + // set the nft logic addresses (to be consumed by the super token factory logic constructor) + cofNFTLogicAddress = constantOutflowNFTLogic.address; + cifNFTLogicAddress = constantInflowNFTLogic.address; - // set the nft logic addresses (to be consumed by the super token factory logic constructor) - cofNFTLogicAddress = constantOutflowNFTLogic.address; - cifNFTLogicAddress = constantInflowNFTLogic.address; + // initialize the nft proxy with the nft logic + await constantOutflowNFTProxy.initializeProxy( + constantOutflowNFTLogic.address + ); + await constantInflowNFTProxy.initializeProxy( + constantInflowNFTLogic.address + ); + const constantOutflowNFT = await ConstantOutflowNFT.at( + constantOutflowNFTProxy.address + ); + const constantInflowNFT = await ConstantInflowNFT.at( + constantInflowNFTProxy.address + ); - // initialize the nft proxy with the nft logic - await constantOutflowNFTProxy.initializeProxy( - constantOutflowNFTLogic.address - ); - await constantInflowNFTProxy.initializeProxy( - constantInflowNFTLogic.address - ); - const constantOutflowNFT = await ConstantOutflowNFT.at( - constantOutflowNFTProxy.address - ); - const constantInflowNFT = await ConstantInflowNFT.at( - constantInflowNFTProxy.address - ); + // initialize the proxy contracts with the nft names + await constantOutflowNFT.initialize( + "Constant Outflow NFT", + "COF" + ); + await constantInflowNFT.initialize( + "Constant Inflow NFT", + "CIF" + ); - // initialize the proxy contracts with the nft names - await constantOutflowNFT.initialize("Constant Outflow NFT", "COF"); - await constantInflowNFT.initialize("Constant Inflow NFT", "CIF"); + // set the nft proxy addresses (to be consumed by the super token logic constructor) + cofNFTProxyAddress = constantOutflowNFTProxy.address; + cifNFTProxyAddress = constantInflowNFTProxy.address; + } + if ( + poolAdminNFTProxyAddress === ZERO_ADDRESS || + poolMemberNFTProxyAddress === ZERO_ADDRESS + ) { + const poolAdminNFTProxy = await web3tx( + UUPSProxy.new, + `Create PoolAdminNFT proxy` + )(); + console.log( + "PoolAdminNFT Proxy address", + poolAdminNFTProxy.address + ); + output += `POOL_ADMIN_NFT_PROXY=${poolAdminNFTProxy.address}\n`; + + const poolMemberNFTProxy = await web3tx( + UUPSProxy.new, + `Create PoolMemberNFT proxy` + )(); + console.log( + "PoolMemberNFT Proxy address", + poolMemberNFTProxy.address + ); + output += `POOL_MEMBER_NFT_PROXY=${poolMemberNFTProxy.address}\n`; + + const poolAdminNFTLogic = await deployNFTContract( + PoolAdminNFT, + "PoolAdminNFT", + "POOL_ADMIN_NFT", + [superfluid.address] + ); + const poolMemberNFTLogic = await deployNFTContract( + PoolMemberNFT, + "PoolMemberNFT", + "POOL_MEMBER_NFT", + [superfluid.address] + ); + + // set the nft logic addresses (to be consumed by the super token factory logic constructor) + poolAdminNFTLogicAddress = poolAdminNFTLogic.address; + poolMemberNFTLogicAddress = poolMemberNFTLogic.address; + + // initialize the nft proxy with the nft logic + await poolAdminNFTProxy.initializeProxy( + poolAdminNFTLogic.address + ); + + await poolMemberNFTProxy.initializeProxy( + poolMemberNFTLogic.address + ); + + const poolAdminNFT = await PoolAdminNFT.at( + poolAdminNFTProxy.address + ); - // set the nft proxy addresses (to be consumed by the super token logic constructor) - cofNFTProxyAddress = constantOutflowNFTProxy.address; - cifNFTProxyAddress = constantInflowNFTProxy.address; + const poolMemberNFT = await PoolMemberNFT.at( + poolMemberNFTProxy.address + ); + + // initialize the proxy contracts with the nft names + await poolAdminNFT.initialize("Pool Admin NFT", "PA"); + await poolMemberNFT.initialize("Pool Member NFT", "PM"); + + // set the nft proxy addresses (to be consumed by the super token logic constructor) + poolAdminNFTProxyAddress = poolAdminNFTProxy.address; + poolMemberNFTProxyAddress = poolMemberNFTProxy.address; + } } else { // nft proxies already exist await deployContractIf( @@ -795,10 +1097,12 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( return constantOutflowNFTLogicChanged; }, async () => { - const cofNFTLogic = await deployNFTContract(true, [ - superfluid.address, - cifNFTProxyAddress, - ]); + const cofNFTLogic = await deployNFTContract( + ConstantOutflowNFT, + "ConstantOutflowNFT", + "CONSTANT_OUTFLOW_NFT", + [superfluid.address, cifNFTProxyAddress] + ); // @note we set the cofNFTLogicAddress to be passed to SuperTokenFactoryLogic here cofNFTLogicAddress = cofNFTLogic.address; @@ -812,15 +1116,56 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( return constantInflowNFTLogicChanged; }, async () => { - const cifNFTLogic = await deployNFTContract(false, [ - superfluid.address, - cofNFTProxyAddress, - ]); + const cifNFTLogic = await deployNFTContract( + ConstantInflowNFT, + "ConstantInflowNFT", + "CONSTANT_INFLOW_NFT", + [ + superfluid.address, + cofNFTProxyAddress, + ] + ); // @note we set the cifNFTLogicAddress to be passed to SuperTokenFactoryLogic here cifNFTLogicAddress = cifNFTLogic.address; return cifNFTLogic.address; } ); + await deployContractIf( + web3, + PoolAdminNFT, + async () => { + return poolAdminNFTLogicChanged; + }, + async () => { + const poolAdminNFTLogic = await deployNFTContract( + PoolAdminNFT, + "PoolAdminNFT", + "POOL_ADMIN_NFT", + [superfluid.address] + ); + // @note we set the poolAdminNFTLogicAddress to be passed to SuperTokenFactoryLogic here + poolAdminNFTLogicAddress = poolAdminNFTLogic.address; + return poolAdminNFTLogic.address; + } + ); + await deployContractIf( + web3, + PoolMemberNFT, + async () => { + return poolMemberNFTLogicChanged; + }, + async () => { + const poolMemberNFTLogic = await deployNFTContract( + PoolMemberNFT, + "PoolMemberNFT", + "POOL_MEMBER_NFT", + [superfluid.address] + ); + // @note we set the poolMemberNFTLogicAddress to be passed to SuperTokenFactoryLogic here + poolMemberNFTLogicAddress = poolMemberNFTLogic.address; + return poolMemberNFTLogic.address; + } + ); } // deploy super token logic contract @@ -830,18 +1175,22 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( superfluid.address, 0, cofNFTProxyAddress, - cifNFTProxyAddress + cifNFTProxyAddress, + poolAdminNFTProxyAddress, + poolMemberNFTProxyAddress ) : await web3tx(SuperTokenLogic.new, "SuperTokenLogic.new")( superfluid.address, cofNFTProxyAddress, - cifNFTProxyAddress + cifNFTProxyAddress, + poolAdminNFTProxyAddress, + poolMemberNFTProxyAddress ); console.log( `SuperToken new logic code address ${superTokenLogic.address}` ); - output += `SUPERFLUID_SUPER_TOKEN_LOGIC=${superTokenLogic.address}\n`; + output += `SUPER_TOKEN_LOGIC=${superTokenLogic.address}\n`; superTokenFactoryLogic = await web3tx( SuperTokenFactoryLogic.new, @@ -850,9 +1199,11 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( superfluid.address, superTokenLogic.address, cofNFTLogicAddress, - cifNFTLogicAddress + cifNFTLogicAddress, + poolAdminNFTLogicAddress, + poolMemberNFTLogicAddress ); - output += `SUPERFLUID_SUPER_TOKEN_FACTORY_LOGIC=${superTokenFactoryLogic.address}\n`; + output += `SUPER_TOKEN_FACTORY_LOGIC=${superTokenFactoryLogic.address}\n`; return superTokenFactoryLogic.address; } ); @@ -878,11 +1229,109 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( superfluid.address, superfluidNewLogicAddress, agreementsToUpdate, - superTokenFactoryNewLogicAddress + superTokenFactoryNewLogicAddress, + ZERO_ADDRESS ) ); } + // Superfluid Pool Beacon deployment + const gdaV1Contract = await GeneralDistributionAgreementV1.at( + await superfluid.getAgreementClass.call(GDAv1_TYPE) + ); + const superfluidPoolBeaconAddress = + await gdaV1Contract.superfluidPoolBeacon(); + + const getPoolLogicAddress = async () => { + if (superfluidPoolBeaconAddress === ZERO_ADDRESS) { + return ZERO_ADDRESS; + } + + try { + return await ( + await SuperfluidUpgradeableBeacon.at( + superfluidPoolBeaconAddress + ) + ).implementation(); + } catch (e) { + return ZERO_ADDRESS; + } + }; + const superfluidPoolLogicAddress = await deployContractIfCodeChanged( + web3, + SuperfluidPool, + await getPoolLogicAddress(), + async () => { + // Deploy new SuperfluidPool logic contract + const superfluidPoolLogic = await web3tx( + SuperfluidPool.new, + "SuperfluidPool.new" + )(gdaV1Contract.address); + await superfluidPoolLogic.castrate(); + console.log( + "New SuperfluidPoolLogic address", + superfluidPoolLogic.address + ); + output += `SUPERFLUID_POOL_LOGIC=${superfluidPoolLogic.address}\n`; + + return superfluidPoolLogic.address; + }, + [ + // See SuperToken constructor parameter + gdaV1Contract.address.toLowerCase().slice(2).padStart(64, "0"), + ] + ); + + // if beacon doesn't exist, we deploy a new one + if (superfluidPoolBeaconAddress === ZERO_ADDRESS) { + console.log( + "SuperfluidPool Beacon doesn't exist, creating a new one..." + ); + const superfluidPoolBeaconContract = await web3tx( + SuperfluidUpgradeableBeacon.new, + "SuperfluidUpgradeableBeacon.new" + )(superfluidPoolLogicAddress); + console.log( + "New SuperfluidPoolBeacon address", + superfluidPoolBeaconContract.address + ); + output += `SUPERFLUID_POOL_BEACON=${superfluidPoolBeaconContract.address}\n`; + + console.log("Transferring ownership of beacon contract to Superfluid Host..."); + await superfluidPoolBeaconContract.transferOwnership(superfluid.address); + + console.log("Initializing GDA w/ beacon contract..."); + await gdaV1Contract.initialize(superfluidPoolBeaconContract.address); + } else { + console.log("Superfluid Pool Beacon exists..."); + // if the beacon exists AND we deployed a new SuperfluidPool logic contract + if (superfluidPoolLogicAddress !== ZERO_ADDRESS) { + console.log( + "superfluidPoolLogicAddress updated, upgrading logic contract..." + ); + // update beacon implementation + const superfluidPoolBeacon = await SuperfluidUpgradeableBeacon.at( + superfluidPoolBeaconAddress + ); + await superfluidPoolBeacon.upgradeTo(superfluidPoolLogicAddress); + } + } + + // finally, set the version string in resolver + if (previousVersionString !== versionString) { + const sfObjForResolver = { + contracts: { + Resolver, + IAccessControlEnumerable, + }, + resolver: { + address: resolver.address + } + }; + const encodedVersionString = versionStringToPseudoAddress(versionString); + await setResolver(sfObjForResolver, `versionString.${protocolReleaseVersion}`, encodedVersionString); + } + // finally, set the version string in resolver if (previousVersionString !== versionString) { diff --git a/packages/ethereum-contracts/ops-scripts/info-print-contract-addresses.js b/packages/ethereum-contracts/ops-scripts/info-print-contract-addresses.js index 5342a0f8e1..c74400b43f 100644 --- a/packages/ethereum-contracts/ops-scripts/info-print-contract-addresses.js +++ b/packages/ethereum-contracts/ops-scripts/info-print-contract-addresses.js @@ -56,10 +56,17 @@ module.exports = eval(`(${S.toString()})()`)(async function ( const SuperToken = artifacts.require("SuperToken"); const UUPSProxiable = artifacts.require("UUPSProxiable"); const ISuperTokenFactory = artifacts.require("ISuperTokenFactory"); + const GeneralDistributionAgreementV1 = artifacts.require( + "GeneralDistributionAgreementV1" + ); + const SuperfluidUpgradeableBeacon = artifacts.require( + "SuperfluidUpgradeableBeacon" + ); if (config.isTestnet) { output += "IS_TESTNET=1\n"; } + output += `NETWORK_ID=${networkId}\n`; output += `RESOLVER=${sf.resolver.address}\n`; output += `SUPERFLUID_LOADER=${sf.loader.address}\n`; @@ -80,20 +87,20 @@ module.exports = eval(`(${S.toString()})()`)(async function ( // ignore } - output += `SUPERFLUID_SUPER_TOKEN_FACTORY_PROXY=${await sf.host.getSuperTokenFactory()}\n`; - output += `SUPERFLUID_SUPER_TOKEN_FACTORY_LOGIC=${await sf.host.getSuperTokenFactoryLogic()}\n`; + output += `SUPER_TOKEN_FACTORY_PROXY=${await sf.host.getSuperTokenFactory()}\n`; + output += `SUPER_TOKEN_FACTORY_LOGIC=${await sf.host.getSuperTokenFactoryLogic()}\n`; output += `CFA_PROXY=${sf.agreements.cfa.address}\n`; output += `CFA_LOGIC=${await getCodeAddress( UUPSProxiable, sf.agreements.cfa.address )}\n`; output += `IDA_PROXY=${sf.agreements.ida.address}\n`; - output += `SLOTS_BITMAP_LIBRARY_ADDRESS=${ + output += `SLOTS_BITMAP_LIBRARY=${ "0x" + ( await web3.eth.call({ to: sf.agreements.ida.address, - data: "0x3fd4176a", //SLOTS_BITMAP_LIBRARY_ADDRESS() + data: "0x3fd4176a", //SLOTS_BITMAP_LIBRARY() }) ).slice(-40) }\n`; @@ -101,10 +108,35 @@ module.exports = eval(`(${S.toString()})()`)(async function ( UUPSProxiable, sf.agreements.ida.address )}\n`; + const gdaProxy = await sf.host.getAgreementClass( + web3.utils.sha3( + "org.superfluid-finance.agreements.GeneralDistributionAgreement.v1" + ) + ); + output += `GDA_PROXY=${gdaProxy}\n`; + output += `GDA_SLOTS_BITMAP_LIBRARY=${ + "0x" + + ( + await web3.eth.call({ + to: gdaProxy, + data: "0x3fd4176a", //SLOTS_BITMAP_LIBRARY() + }) + ).slice(-40) + }\n`; + output += `GDA_LOGIC=${await getCodeAddress(UUPSProxiable, gdaProxy)}\n`; + + const gdaContract = await GeneralDistributionAgreementV1.at(gdaProxy); + const superfluidPoolBeaconContract = await SuperfluidUpgradeableBeacon.at( + await gdaContract.superfluidPoolBeacon() + ); + output += `SUPERFLUID_POOL_DEPLOYER=${await gdaContract.SUPERFLUID_POOL_DEPLOYER_ADDRESS()}\n`; + output += `SUPERFLUID_POOL_BEACON=${superfluidPoolBeaconContract.address}\n`; + output += `SUPERFLUID_POOL_LOGIC=${await superfluidPoolBeaconContract.implementation()}\n`; + const superTokenLogicAddress = await ( await ISuperTokenFactory.at(await sf.host.getSuperTokenFactory()) ).getSuperTokenLogic(); - output += `SUPERFLUID_SUPER_TOKEN_LOGIC=${superTokenLogicAddress}\n`; + output += `SUPER_TOKEN_LOGIC=${superTokenLogicAddress}\n`; const superTokenLogicContract = await SuperToken.at(superTokenLogicAddress); @@ -125,6 +157,24 @@ module.exports = eval(`(${S.toString()})()`)(async function ( await UUPSProxiable.at(constantInflowNFTProxyAddress) ).getCodeAddress(); output += `CONSTANT_INFLOW_NFT_LOGIC=${constantInflowNFTLogicAddress}\n`; + + const poolAdminNFTProxyAddress = + await superTokenLogicContract.POOL_ADMIN_NFT(); + output += `POOL_ADMIN_NFT_PROXY=${poolAdminNFTProxyAddress}\n`; + + const poolAdminNFTLogicAddress = await ( + await UUPSProxiable.at(poolAdminNFTProxyAddress) + ).getCodeAddress(); + output += `POOL_ADMIN_NFT_LOGIC=${poolAdminNFTLogicAddress}\n`; + + const poolMemberNFTProxyAddress = + await superTokenLogicContract.POOL_MEMBER_NFT(); + output += `POOL_MEMBER_NFT_PROXY=${poolMemberNFTProxyAddress}\n`; + + const poolMemberNFTLogicAddress = await ( + await UUPSProxiable.at(poolMemberNFTProxyAddress) + ).getCodeAddress(); + output += `POOL_MEMBER_NFT_LOGIC=${poolMemberNFTLogicAddress}\n`; if (! skipTokens) { await Promise.all( diff --git a/packages/ethereum-contracts/ops-scripts/libs/common.js b/packages/ethereum-contracts/ops-scripts/libs/common.js index 448f312991..7cc7fdf92c 100644 --- a/packages/ethereum-contracts/ops-scripts/libs/common.js +++ b/packages/ethereum-contracts/ops-scripts/libs/common.js @@ -80,6 +80,7 @@ async function hasCode(web3, address) { /** * @dev Check if the code at the address differs from the contract object provided + * @param replacements should contain all immutable contract fields, encoded as words * TODO: this isn't always working as intended, see https://github.com/superfluid-finance/protocol-monorepo/issues/1448 */ async function codeChanged( diff --git a/packages/ethereum-contracts/ops-scripts/libs/getConfig.js b/packages/ethereum-contracts/ops-scripts/libs/getConfig.js index cb0b2380c9..98bb0cb0da 100644 --- a/packages/ethereum-contracts/ops-scripts/libs/getConfig.js +++ b/packages/ethereum-contracts/ops-scripts/libs/getConfig.js @@ -46,6 +46,7 @@ module.exports = function getConfig(chainId) { getLogsRange: sfNw?.logsQueryRange || 5000, }, cfaFwd: sfNw?.contractsV1?.cfaV1Forwarder || "0xcfA132E353cB4E398080B9700609bb008eceB125", + gdaFwd: sfNw?.contractsV1?.gdaV1Forwarder || "0x6dA170169d5Fca20F902b7E5755346a97c94B07c", nativeTokenSymbol: sfNw?.nativeTokenSymbol || "ETH", metadata: sfNw, resolverAddress: global?.process.env.RESOLVER_ADDRESS || sfNw?.contractsV1?.resolver, diff --git a/packages/ethereum-contracts/ops-scripts/validate-deployment.ts b/packages/ethereum-contracts/ops-scripts/validate-deployment.ts new file mode 100644 index 0000000000..0495f223b7 --- /dev/null +++ b/packages/ethereum-contracts/ops-scripts/validate-deployment.ts @@ -0,0 +1,143 @@ +import { assert, ethers } from "hardhat"; +import metadata from "@superfluid-finance/metadata"; + +const cfaAgreementType = ethers.utils.solidityKeccak256(["string"], ["org.superfluid-finance.agreements.ConstantFlowAgreement.v1"]); +const idaAgreementType = ethers.utils.solidityKeccak256(["string"], ["org.superfluid-finance.agreements.InstantDistributionAgreement.v1"]); +const gdaAgreementType = ethers.utils.solidityKeccak256(["string"], ["org.superfluid-finance.agreements.GeneralDistributionAgreement.v1"]); + +const superTokenFactoryUuid = ethers.utils.solidityKeccak256(["string"], ["org.superfluid-finance.contracts.SuperTokenFactory.implementation"]); +const superTokenUUID = ethers.utils.solidityKeccak256(["string"], ["org.superfluid-finance.contracts.SuperToken.implementation"]); +const constantOutflowNftUuid = ethers.utils.solidityKeccak256(["string"], ["org.superfluid-finance.contracts.ConstantOutflowNFT.implementation"]); +const constantInflowNftUuid = ethers.utils.solidityKeccak256(["string"], ["org.superfluid-finance.contracts.ConstantInflowNFT.implementation"]); +const superfluidPoolUUID = ethers.utils.solidityKeccak256(["string"], ["org.superfluid-finance.contracts.SuperfluidPool.implementation"]); + +function assertLog(condition: boolean, message: string) { + console.log("ASSERTING:", message); + assert(condition, "[ASSERTION ERROR]: " + message); + console.log("ASSERTION PASSED!", "\n") +} + +async function main() { + const networkId = (await ethers.provider.getNetwork()).chainId; + const networkMetadata = metadata.getNetworkByChainId(networkId); + + if (networkMetadata === undefined) { + throw new Error("Network not supported"); + } + + const RESOLVER_ADDRESS = networkMetadata.contractsV1.resolver; + + const resolver = await ethers.getContractAt("Resolver", RESOLVER_ADDRESS || ""); + + const hostAddress = await resolver.get("Superfluid.v1"); + const hostContract = await ethers.getContractAt("Superfluid", hostAddress); + + const superTokenFactoryAddress = await hostContract.getSuperTokenFactory(); + const superTokenFactoryContract = await ethers.getContractAt("SuperTokenFactory", superTokenFactoryAddress); + console.log("SuperTokenFactory Address:", superTokenFactoryAddress, "\n"); + const superTokenFactoryLiveUUID = await superTokenFactoryContract.proxiableUUID(); + assertLog(superTokenFactoryUuid === superTokenFactoryLiveUUID, "SuperTokenFactory Deployed UUID matches live UUID"); + + const isCFAv1ForwarderATrustedForwarder = await hostContract.isTrustedForwarder(networkMetadata.contractsV1.cfaV1Forwarder); + assertLog(isCFAv1ForwarderATrustedForwarder, "CFAv1 Forwarder is set as trusted forwarder"); + + const superTokenFactoryLogicAddress = await hostContract.getSuperTokenFactoryLogic(); + console.log("SuperTokenFactory Logic Address:", superTokenFactoryLogicAddress, "\n"); + + assertLog(superTokenFactoryLogicAddress === await superTokenFactoryContract.getCodeAddress(), "Canonical Factory Logic Address matches Factory Proxy Logic Address"); + + const superTokenLogicAddress = await superTokenFactoryContract.getSuperTokenLogic(); + console.log("SuperToken Logic Address:", superTokenLogicAddress, "\n"); + + const superTokenLogicContract = await ethers.getContractAt("SuperToken", superTokenLogicAddress); + const superTokenLiveUUID = await superTokenLogicContract.proxiableUUID(); + assertLog(superTokenUUID === superTokenLiveUUID, "SuperTokenFactory Deployed UUID matches live UUID"); + + // validate flow NFTs + const constantOutflowNFTCanonicalLogic = await superTokenFactoryContract.CONSTANT_OUTFLOW_NFT_LOGIC(); + console.log("ConstantOutflowNFT Canonical Logic (on Factory):", constantOutflowNFTCanonicalLogic); + const constantInflowNFTCanonicalLogic = await superTokenFactoryContract.CONSTANT_INFLOW_NFT_LOGIC(); + console.log("ConstantInflowNFT Canonical Logic (on Factory):", constantInflowNFTCanonicalLogic, "\n"); + + const constantOutflowNFProxy = await superTokenLogicContract.CONSTANT_OUTFLOW_NFT(); + const cofNFTContract = await ethers.getContractAt("ConstantOutflowNFT", constantOutflowNFProxy); + const cofNFTContractLiveUUID = await cofNFTContract.proxiableUUID(); + assertLog(constantOutflowNftUuid === cofNFTContractLiveUUID, "ConstantOutflowNFT Deployed UUID matches live UUID"); + console.log("ConstantOutflowNFT:", constantOutflowNFProxy); + + const outflowProxyLogic = await cofNFTContract.getCodeAddress(); + console.log("ConstantOutflow NFT Logic (on Proxy):", outflowProxyLogic, "\n"); + assertLog(await cofNFTContract.baseURI() === "https://nft.superfluid.finance/cfa/v2/getmeta", "ConstantOutflowNFT baseURI is equal to https://nft.superfluid.finance/cfa/v2/getmeta"); + + const constantInflowNFProxy = await superTokenLogicContract.CONSTANT_INFLOW_NFT(); + const cifNFTContract = await ethers.getContractAt("ConstantInflowNFT", constantInflowNFProxy); + const cifNFTContractLiveUUID = await cifNFTContract.proxiableUUID(); + assertLog(constantInflowNftUuid === cifNFTContractLiveUUID, "ConstantInflowNFT Deployed UUID matches live UUID"); + console.log("ConstantInflowNFT:", constantInflowNFProxy); + assertLog(await cifNFTContract.baseURI() === "https://nft.superfluid.finance/cfa/v2/getmeta", "ConstantInflowNFT baseURI is equal to https://nft.superfluid.finance/cfa/v2/getmeta"); + + const inflowProxyLogic = await cifNFTContract.getCodeAddress(); + console.log("ConstantInflow NFT Logic (on Proxy):", inflowProxyLogic); + + assertLog(await cofNFTContract.proxiableUUID() !== await cifNFTContract.proxiableUUID(), "NFT proxies have different implementation."); + + assertLog(outflowProxyLogic === constantOutflowNFTCanonicalLogic, "Outflow proxy logic is equal to canonical outflow logic"); + assertLog(inflowProxyLogic === constantInflowNFTCanonicalLogic, "Inflow proxy logic is equal to canonical inflow logic"); + + // validate pool NFTs + const poolAdminNFTCanonicalLogic = await superTokenFactoryContract.POOL_ADMIN_NFT_LOGIC(); + console.log("PoolAdminNFT Canonical Logic (on Factory):", poolAdminNFTCanonicalLogic); + const poolMemberNFTCanonicalLogic = await superTokenFactoryContract.POOL_MEMBER_NFT_LOGIC(); + console.log("PoolMemberNFT Canonical Logic (on Factory):", poolMemberNFTCanonicalLogic, "\n"); + + const poolAdminNFProxy = await superTokenLogicContract.POOL_ADMIN_NFT(); + const paNFTContract = await ethers.getContractAt("PoolAdminNFT", poolAdminNFProxy); + console.log("PoolAdminNFT:", poolAdminNFProxy); + const poolAdminProxyLogic = await paNFTContract.getCodeAddress(); + console.log("PoolAdmin NFT Logic (on Proxy):", poolAdminProxyLogic, "\n"); + assertLog(await paNFTContract.baseURI() === "https://nft.superfluid.finance/pool/v2/getmeta", "PoolAdminNFT baseURI is equal to https://nft.superfluid.finance/pool/v2/getmeta"); + + const poolMemberNFProxy = await superTokenLogicContract.POOL_MEMBER_NFT(); + console.log("PoolMemberNFT:", poolMemberNFProxy); + const pmNFTContract = await ethers.getContractAt("PoolMemberNFT", poolMemberNFProxy); + assertLog(await pmNFTContract.baseURI() === "https://nft.superfluid.finance/pool/v2/getmeta", "PoolMemberNFT baseURI is equal to https://nft.superfluid.finance/pool/v2/getmeta"); + + const poolMemberProxyLogic = await pmNFTContract.getCodeAddress(); + console.log("ConstantInflow NFT Logic (on Proxy):", poolMemberProxyLogic); + + assertLog(await paNFTContract.proxiableUUID() !== await pmNFTContract.proxiableUUID(), "NFT proxies have different implementation."); + + assertLog(poolAdminProxyLogic === poolAdminNFTCanonicalLogic, "Pool admin proxy logic is equal to canonical pool admin logic"); + assertLog(poolMemberProxyLogic === poolMemberNFTCanonicalLogic, "Pool member proxy logic is equal to canonical pool member logic"); + + const cfaAddress = await hostContract.getAgreementClass(cfaAgreementType); + const idaAddress = await hostContract.getAgreementClass(idaAgreementType); + const gdaAddress = await hostContract.getAgreementClass(gdaAgreementType); + + assertLog(cfaAddress !== ethers.constants.AddressZero, "CFA Address is not zero address"); + assertLog(idaAddress !== ethers.constants.AddressZero, "IDA Address is not zero address"); + assertLog(gdaAddress !== ethers.constants.AddressZero, "GDA Address is not zero address"); + + const cfaContract = await ethers.getContractAt("ConstantFlowAgreementV1", cfaAddress); + const idaContract = await ethers.getContractAt("InstantDistributionAgreementV1", idaAddress); + const gdaContract = await ethers.getContractAt("GeneralDistributionAgreementV1", gdaAddress); + + assertLog(await cfaContract.agreementType() === cfaAgreementType, "CFA AgreementType is equal to expected agreementType") + assertLog(await idaContract.agreementType() === idaAgreementType, "IDA AgreementType is equal to expected agreementType") + assertLog(await gdaContract.agreementType() === gdaAgreementType, "GDA AgreementType is equal to expected agreementType") + + // GDA specific validation + const superfluidPoolBeaconAddress = await gdaContract.superfluidPoolBeacon(); + assertLog(superfluidPoolBeaconAddress !== ethers.constants.AddressZero, "SuperfluidPoolBeaconAddress is not zero address") + + const beaconContract = await ethers.getContractAt("IBeacon", superfluidPoolBeaconAddress); + const sfPoolBeaconImplementationAddress = await beaconContract.implementation(); + assertLog(sfPoolBeaconImplementationAddress !== ethers.constants.AddressZero, "SFPool beacon implementation is not zero address"); + + const superfluidPoolContract = await ethers.getContractAt("SuperfluidPool", sfPoolBeaconImplementationAddress); + const sfPoolLiveUUID = await superfluidPoolContract.proxiableUUID(); + + assertLog(sfPoolLiveUUID === superfluidPoolUUID, "SFPool Deployed UUID is equal to expected SFPool UUID"); +} + +main(); \ No newline at end of file diff --git a/packages/ethereum-contracts/ops-scripts/validate-nft-addresses.ts b/packages/ethereum-contracts/ops-scripts/validate-nft-addresses.ts deleted file mode 100644 index 6ff3177e0a..0000000000 --- a/packages/ethereum-contracts/ops-scripts/validate-nft-addresses.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ethers } from "hardhat"; -import metadata from "@superfluid-finance/metadata"; - -async function main() { - const networkId = (await ethers.provider.getNetwork()).chainId; - const RESOLVER_ADDRESS = metadata.getNetworkByChainId(networkId)?.contractsV1.resolver; - const resolver = await ethers.getContractAt("Resolver", RESOLVER_ADDRESS || ""); - const hostAddress = await resolver.get("Superfluid.v1"); - const hostContract = await ethers.getContractAt("Superfluid", hostAddress); - const superTokenFactoryAddress = await hostContract.getSuperTokenFactory(); - const superTokenFactoryContract = await ethers.getContractAt("SuperTokenFactory", superTokenFactoryAddress); - console.log("superTokenFactoryAddress", superTokenFactoryAddress); - - const constantOutflowNFTCanonicalLogic = await superTokenFactoryContract.CONSTANT_OUTFLOW_NFT_LOGIC(); - console.log("constantOutflowNFTCanonicalLogic", constantOutflowNFTCanonicalLogic); - const constantInflowNFTCanonicalLogic = await superTokenFactoryContract.CONSTANT_INFLOW_NFT_LOGIC(); - console.log("constantInflowNFTCanonicalLogic", constantInflowNFTCanonicalLogic); - - const superTokenFactoryLogicAddress = await hostContract.getSuperTokenFactoryLogic(); - console.log("superTokenFactoryLogicAddress", superTokenFactoryLogicAddress); - const superTokenLogicAddress = await superTokenFactoryContract.getSuperTokenLogic(); - const superTokenLogicContract = await ethers.getContractAt("SuperToken", superTokenLogicAddress); - - const constantOutflowNFProxy = await superTokenLogicContract.CONSTANT_OUTFLOW_NFT(); - const cofNFTContract = await ethers.getContractAt("ConstantOutflowNFT", constantOutflowNFProxy); - console.log("constantOutflowNFProxy", constantOutflowNFProxy); - const outflowProxyLogic = await cofNFTContract.getCodeAddress(); - console.log("outflowProxyLogic", outflowProxyLogic); - console.log("cof baseURI", await cofNFTContract.baseURI()); - - const constantInflowNFProxy = await superTokenLogicContract.CONSTANT_INFLOW_NFT(); - console.log("constantInflowNFProxy", constantInflowNFProxy); - const cifNFTContract = await ethers.getContractAt("ConstantInflowNFT", constantInflowNFProxy); - const inflowProxyLogic = await cifNFTContract.getCodeAddress(); - console.log("inflowProxyLogic", inflowProxyLogic); - const differentImplementations = await cofNFTContract.proxiableUUID() !== await cifNFTContract.proxiableUUID(); - console.log("nft's have different implementations:", differentImplementations); - console.log("cif baseURI", await cofNFTContract.baseURI()); - - if (!differentImplementations) throw new Error("nft's have the same implementation"); - - console.log("outflow proxy logic equal canonical logic", outflowProxyLogic === constantOutflowNFTCanonicalLogic); - console.log("inflow proxy logic equal canonical logic", inflowProxyLogic === constantInflowNFTCanonicalLogic); - - if (outflowProxyLogic !== constantOutflowNFTCanonicalLogic) throw new Error("outflow proxy logic not equal canonical logic"); - if (inflowProxyLogic !== constantInflowNFTCanonicalLogic) throw new Error("inflow proxy logic not equal canonical logic"); -} - -main(); \ No newline at end of file diff --git a/packages/ethereum-contracts/package.json b/packages/ethereum-contracts/package.json index 56ca826e61..afcb7f8ad6 100644 --- a/packages/ethereum-contracts/package.json +++ b/packages/ethereum-contracts/package.json @@ -1,6 +1,6 @@ { "name": "@superfluid-finance/ethereum-contracts", - "version": "1.8.1", + "version": "1.9.0", "description": " Ethereum contracts implementation for the Superfluid Protocol", "homepage": "https://github.com/superfluid-finance/protocol-monorepo/tree/dev/packages/ethereum-contracts#readme", "repository": { @@ -43,7 +43,7 @@ "build:post-contracts": "run-p -l build:post-contracts:*", "build:post-contracts:abi-bundle": "tasks/build-bundled-abi.sh", "build:post-contracts:dev-scripts-typings": "rm -rf dev-scripts/*.d.ts dev-scripts/*.d.ts.map; tsc -p tsconfig.scripts.json", - "build:post-contracts:contracts-size": "forge build --sizes > build/contracts-sizes.txt", + "build:post-contracts:contracts-size": "forge build --sizes > build/contracts-sizes.txt > /dev/null&", "verify-framework": "tasks/etherscan-verify-framework.sh", "testenv:start": "test/testenv-ctl.sh start", "testenv:stop": "test/testenv-ctl.sh stop", diff --git a/packages/ethereum-contracts/tasks/bundled-abi-contracts-list.json b/packages/ethereum-contracts/tasks/bundled-abi-contracts-list.json index 86155c49bb..b531b112c8 100644 --- a/packages/ethereum-contracts/tasks/bundled-abi-contracts-list.json +++ b/packages/ethereum-contracts/tasks/bundled-abi-contracts-list.json @@ -13,11 +13,12 @@ "IFlowNFTBase", "FlowNFTBase", "IConstantInflowNFT", "ConstantInflowNFT", "IConstantOutflowNFT", "ConstantOutflowNFT", - "IPoolAdminNFT", - "IPoolMemberNFT", + "IPoolAdminNFT", "PoolAdminNFT", + "IPoolMemberNFT", "PoolMemberNFT", "ISuperAgreement", "IConstantFlowAgreementV1", "ConstantFlowAgreementV1", "IInstantDistributionAgreementV1", "InstantDistributionAgreementV1", + "IGeneralDistributionAgreementV1", "GeneralDistributionAgreementV1", "ISuperfluidGovernance", "SuperfluidGovernanceBase", "SuperfluidGovernanceII", "TestGovernance", "TestToken", "IPureSuperToken", diff --git a/packages/ethereum-contracts/tasks/coverage-cleanup.sh b/packages/ethereum-contracts/tasks/coverage-cleanup.sh index b371764641..0ff548eac5 100755 --- a/packages/ethereum-contracts/tasks/coverage-cleanup.sh +++ b/packages/ethereum-contracts/tasks/coverage-cleanup.sh @@ -4,12 +4,12 @@ set -ex cd "$(dirname "$0")"/.. -# extract coverage for NFT contracts from forge coverage +# extract coverage for Superfluid contracts from forge coverage lcov -e ../../lcov.info \ "packages/ethereum-contracts/contracts/*" \ -o lcov.info -# remove mocks, base super app, test and deployer contracts (see .solcover.js) +# remove contracts whose coverage we don't care about (see .solcover.js) lcov -r lcov.info \ "packages/ethereum-contracts/contracts/mocks/*" \ "packages/ethereum-contracts/contracts/apps/*Base*" \ diff --git a/packages/ethereum-contracts/tasks/deploy-cfa-forwarder.sh b/packages/ethereum-contracts/tasks/deploy-cfa-forwarder.sh index a9135444a0..0e6151c198 100755 --- a/packages/ethereum-contracts/tasks/deploy-cfa-forwarder.sh +++ b/packages/ethereum-contracts/tasks/deploy-cfa-forwarder.sh @@ -1,42 +1,54 @@ #!/usr/bin/env bash -set -eux +set -eu # Usage: -# tasks/deploy-cfa-forwarder.sh [] +# tasks/deploy-cfa-forwarder.sh # # Example: -# tasks/deploy-cfa-forwarder.sh optimism-goerli 0xcfa132e353cb4e398080b9700609bb008eceb125 +# tasks/deploy-cfa-forwarder.sh optimism-goerli # # The invoking account needs to be (co-)owner of the resolver and governance # # important ENV vars: -# RELEASE_VERSION, DETERMINISTIC_DEPLOYER_PK, RESOLVER_ADMIN_TYPE, GOVERNANCE_ADMIN_TYPE +# RELEASE_VERSION, CFAFWD_DEPLOYER_PK # # You can use the npm package vanity-eth to get a deployer account for a given contract address: # Example use: npx vanityeth -i cfa1 --contract # -# Note that the value of DETERMINISTIC_DEPLOYER_PK needs to match the given contract-addr. -# The script will not check this, but fail (at contract verification) if not matching. -# -# For optimism the gas estimation doesn't work, requires setting EST_TX_COST. -# For polygon-mainnet, GAS_PRICE usually needs to be set. +# For optimism the gas estimation doesn't work, requires setting EST_TX_COST +# (the value auto-detected for arbitrum should work). # # On some networks you may need to use override ENV vars for the deployment to succeed +# shellcheck source=/dev/null +source .env + +set -x + network=$1 -cfaFwdAddr=${2:-0xcfA132E353cB4E398080B9700609bb008eceB125} +expectedContractAddr="0xcfA132E353cB4E398080B9700609bb008eceB125" +deployerPk=$CFAFWD_DEPLOYER_PK +tmpfile="/tmp/deploy-cfa-forwarder.sh" # deploy -npx truffle exec --network "$network" ops-scripts/deploy-deterministically.js : CFAv1Forwarder +DETERMINISTIC_DEPLOYER_PK=$deployerPk npx truffle exec --network "$network" ops-scripts/deploy-deterministically.js : CFAv1Forwarder | tee $tmpfile +contractAddr=$(cat $tmpfile | tail -n 1) +rm $tmpfile + +echo "deployed to $contractAddr" +if [[ $contractAddr != "$expectedContractAddr" ]]; then + echo "oh no!" + exit +fi # verify (give it a few seconds to pick up the code) sleep 5 -npx truffle run --network "$network" verify CFAv1Forwarder@"$cfaFwdAddr" +npx truffle run --network "$network" verify CFAv1Forwarder@"$contractAddr" # set resolver -ALLOW_UPDATE=1 npx truffle exec --network "$network" ops-scripts/resolver-set-key-value.js : CFAv1Forwarder "$cfaFwdAddr" +ALLOW_UPDATE=1 npx truffle exec --network "$network" ops-scripts/resolver-set-key-value.js : CFAv1Forwarder "$contractAddr" # create gov action -npx truffle exec --network "$network" ops-scripts/gov-set-trusted-forwarder.js : 0x0000000000000000000000000000000000000000 "$cfaFwdAddr" 1 +npx truffle exec --network "$network" ops-scripts/gov-set-trusted-forwarder.js : 0x0000000000000000000000000000000000000000 "$contractAddr" 1 # TODO: on mainnets, the resolver entry should be set only after the gov action was signed & executed diff --git a/packages/ethereum-contracts/tasks/deploy-gda-forwarder.sh b/packages/ethereum-contracts/tasks/deploy-gda-forwarder.sh new file mode 100755 index 0000000000..08da123704 --- /dev/null +++ b/packages/ethereum-contracts/tasks/deploy-gda-forwarder.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -eu + +# Usage: +# tasks/deploy-gda-forwarder.sh +# +# Example: +# tasks/deploy-gda-forwarder.sh optimism-goerli +# +# The invoking account needs to be (co-)owner of the resolver and governance +# +# important ENV vars: +# RELEASE_VERSION, GDAFWD_DEPLOYER_PK +# +# You can use the npm package vanity-eth to get a deployer account for a given contract address: +# Example use: npx vanityeth -i 6da1 --contract +# +# For optimism the gas estimation doesn't work, requires setting EST_TX_COST +# (the value auto-detected for arbitrum should work). +# +# On some networks you may need to use override ENV vars for the deployment to succeed + +# shellcheck source=/dev/null +source .env + +set -x + +network=$1 +expectedContractAddr="0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08" +deployerPk=$GDAFWD_DEPLOYER_PK + +tmpfile="/tmp/deploy-gda-forwarder.sh" +# deploy +DETERMINISTIC_DEPLOYER_PK=$deployerPk npx truffle exec --network "$network" ops-scripts/deploy-deterministically.js : GDAv1Forwarder | tee $tmpfile +contractAddr=$(cat $tmpfile | tail -n 1) +rm $tmpfile + +echo "deployed to $contractAddr" +if [[ $contractAddr != "$expectedContractAddr" ]]; then + echo "oh no!" + exit +fi + +# verify (give it a few seconds to pick up the code) +sleep 5 +npx truffle run --network "$network" verify GDAv1Forwarder@"$contractAddr" + +# set resolver +ALLOW_UPDATE=1 npx truffle exec --network "$network" ops-scripts/resolver-set-key-value.js : GDAv1Forwarder "$contractAddr" + +# create gov action +npx truffle exec --network "$network" ops-scripts/gov-set-trusted-forwarder.js : 0x0000000000000000000000000000000000000000 "$contractAddr" 1 + +# TODO: on mainnets, the resolver entry should be set only after the gov action was signed & executed diff --git a/packages/ethereum-contracts/tasks/etherscan-verify-framework.sh b/packages/ethereum-contracts/tasks/etherscan-verify-framework.sh index 3044c23f6d..880d40dcdf 100755 --- a/packages/ethereum-contracts/tasks/etherscan-verify-framework.sh +++ b/packages/ethereum-contracts/tasks/etherscan-verify-framework.sh @@ -2,6 +2,7 @@ # verification script for etherscan-like explorers. # takes 2 arguments: the canonical network name and a file with a list of contract addresses to verify. +# If additional arguments are provided, they will be added to individual verification commands. # tries to verify the (sub)set of contracts listed in the file. # if proxy addresses are provided, verification against up-to-date logic contracts will only succeed # once they point to those (after gov upgrade execution) @@ -12,6 +13,8 @@ CONTRACTS_DIR=build/truffle TRUFFLE_NETWORK=$1 ADDRESSES_VARS=$2 +shift 2 +EXTRA_ARGS="$*" if [ -z "$ADDRESSES_VARS" ]; then echo "no addresses provided, fetching myself..." @@ -25,7 +28,7 @@ source "$ADDRESSES_VARS" FAILED_VERIFICATIONS=() function try_verify() { echo # newline for better readability - npx truffle run --network "$TRUFFLE_NETWORK" verify "$@" || + npx truffle run --network "$TRUFFLE_NETWORK" verify "$@" ${EXTRA_ARGS:+$EXTRA_ARGS} || FAILED_VERIFICATIONS[${#FAILED_VERIFICATIONS[@]}]="$*" # NOTE: append using length so that having spaces in the element is not a problem # TODO: version 0.6.5 of the plugin seems to not reliably return non-zero if verification fails @@ -57,6 +60,14 @@ if [ -n "$RESOLVER" ]; then try_verify Resolver@"${RESOLVER}" fi +if [ -n "$POOL_ADMIN_NFT_LOGIC" ]; then + try_verify PoolAdminNFT@"${POOL_ADMIN_NFT_LOGIC}" +fi + +if [ -n "$POOL_MEMBER_NFT_LOGIC" ]; then + try_verify PoolMemberNFT@"${POOL_MEMBER_NFT_LOGIC}" +fi + if [ -n "$SUPERFLUID_HOST_LOGIC" ]; then # verify the logic contract. May or may not be already set as a proxy implementation try_verify Superfluid@"${SUPERFLUID_HOST_LOGIC}" @@ -81,11 +92,11 @@ if [ -n "$SUPERFLUID_LOADER" ]; then try_verify SuperfluidLoader@"${SUPERFLUID_LOADER}" fi -if [ -n "$SUPERFLUID_SUPER_TOKEN_FACTORY_LOGIC" ]; then - try_verify SuperTokenFactory@"${SUPERFLUID_SUPER_TOKEN_FACTORY_LOGIC}" +if [ -n "$SUPER_TOKEN_FACTORY_LOGIC" ]; then + try_verify SuperTokenFactory@"${SUPER_TOKEN_FACTORY_LOGIC}" fi -if [ -n "$SUPERFLUID_SUPER_TOKEN_FACTORY_PROXY" ]; then - try_verify SuperTokenFactory@"${SUPERFLUID_SUPER_TOKEN_FACTORY_PROXY}" --custom-proxy UUPSProxy +if [ -n "$SUPER_TOKEN_FACTORY_PROXY" ]; then + try_verify SuperTokenFactory@"${SUPER_TOKEN_FACTORY_PROXY}" --custom-proxy UUPSProxy fi if [ -n "$CONSTANT_OUTFLOW_NFT_LOGIC" ]; then @@ -104,8 +115,16 @@ if [ -n "$CONSTANT_INFLOW_NFT_PROXY" ]; then try_verify ConstantInflowNFT@"${CONSTANT_INFLOW_NFT_PROXY}" --custom-proxy UUPSProxy fi -if [ -n "$SUPERFLUID_SUPER_TOKEN_LOGIC" ]; then - try_verify SuperToken@"${SUPERFLUID_SUPER_TOKEN_LOGIC}" +if [ -n "$POOL_ADMIN_NFT_PROXY" ]; then + try_verify PoolAdminNFT@"${POOL_ADMIN_NFT_PROXY}" --custom-proxy UUPSProxy +fi + +if [ -n "$POOL_MEMBER_NFT_PROXY" ]; then + try_verify PoolMemberNFT@"${POOL_MEMBER_NFT_PROXY}" --custom-proxy UUPSProxy +fi + +if [ -n "$SUPER_TOKEN_LOGIC" ]; then + try_verify SuperToken@"${SUPER_TOKEN_LOGIC}" fi if [ -n "$CFA_LOGIC" ]; then @@ -115,17 +134,40 @@ if [ -n "$CFA_PROXY" ]; then try_verify ConstantFlowAgreementV1@"${CFA_PROXY}" --custom-proxy UUPSProxy fi -if [ -n "$SLOTS_BITMAP_LIBRARY_ADDRESS" ]; then - try_verify SlotsBitmapLibrary@"${SLOTS_BITMAP_LIBRARY_ADDRESS}" +if [ -n "$SLOTS_BITMAP_LIBRARY" ]; then + try_verify SlotsBitmapLibrary@"${SLOTS_BITMAP_LIBRARY}" fi -link_library "InstantDistributionAgreementV1" "SlotsBitmapLibrary" "${SLOTS_BITMAP_LIBRARY_ADDRESS}" +link_library "InstantDistributionAgreementV1" "SlotsBitmapLibrary" "${SLOTS_BITMAP_LIBRARY}" if [ -n "$IDA_LOGIC" ]; then try_verify InstantDistributionAgreementV1@"${IDA_LOGIC}" fi if [ -n "$IDA_PROXY" ]; then try_verify InstantDistributionAgreementV1@"${IDA_PROXY}" --custom-proxy UUPSProxy fi + +if [ -n "$SUPERFLUID_POOL_DEPLOYER" ]; then + try_verify SuperfluidPoolDeployerLibrary@"${SUPERFLUID_POOL_DEPLOYER}" +fi + +link_library "GeneralDistributionAgreementV1" "SlotsBitmapLibrary" "${GDA_SLOTS_BITMAP_LIBRARY}" +link_library "GeneralDistributionAgreementV1" "SuperfluidPoolDeployerLibrary" "${SUPERFLUID_POOL_DEPLOYER}" +if [ -n "$GDA_LOGIC" ]; then + try_verify GeneralDistributionAgreementV1@"${GDA_LOGIC}" +fi + +if [ -n "$GDA_PROXY" ]; then + try_verify GeneralDistributionAgreementV1@"${GDA_PROXY}" --custom-proxy UUPSProxy +fi + +if [ -n "$SUPERFLUID_POOL_BEACON" ]; then + try_verify SuperfluidUpgradeableBeacon@"${SUPERFLUID_POOL_BEACON}" +fi + +if [ -n "$SUPERFLUID_POOL_LOGIC" ]; then + try_verify SuperfluidPool@"${SUPERFLUID_POOL_LOGIC}" +fi + mv -f $CONTRACTS_DIR/InstantDistributionAgreementV1.json.bak $CONTRACTS_DIR/InstantDistributionAgreementV1.json if [ -n "$SUPER_TOKEN_NATIVE_COIN" ];then diff --git a/packages/ethereum-contracts/tasks/etherscan-verify-proxies.sh b/packages/ethereum-contracts/tasks/etherscan-verify-proxies.sh index 2c8b6e19c6..1367c0fc25 100644 --- a/packages/ethereum-contracts/tasks/etherscan-verify-proxies.sh +++ b/packages/ethereum-contracts/tasks/etherscan-verify-proxies.sh @@ -13,8 +13,8 @@ echo TRUFFLE_RUN_VERIFY="npx truffle run --network $TRUFFLE_NETWORK verify" echo SUPERFLUID_HOST UUPSProxy $TRUFFLE_RUN_VERIFY --verify-proxy UUPSProxy@"${SUPERFLUID_HOST_PROXY}" -echo SUPERFLUID_SUPER_TOKEN_FACTORY UUPSProxy -$TRUFFLE_RUN_VERIFY --verify-proxy UUPSProxy@"${SUPERFLUID_SUPER_TOKEN_FACTORY_PROXY}" +echo SUPER_TOKEN_FACTORY UUPSProxy +$TRUFFLE_RUN_VERIFY --verify-proxy UUPSProxy@"${SUPER_TOKEN_FACTORY_PROXY}" echo CFA UUPSProxy $TRUFFLE_RUN_VERIFY --verify-proxy UUPSProxy@"${CFA_PROXY}" diff --git a/packages/ethereum-contracts/test/TestEnvironment.ts b/packages/ethereum-contracts/test/TestEnvironment.ts index f2f0a2f3e9..1985ef56c3 100644 --- a/packages/ethereum-contracts/test/TestEnvironment.ts +++ b/packages/ethereum-contracts/test/TestEnvironment.ts @@ -13,6 +13,10 @@ import { ConstantOutflowNFT__factory, ISuperToken, ISuperToken__factory, + PoolAdminNFT, + PoolAdminNFT__factory, + PoolMemberNFT, + PoolMemberNFT__factory, SuperTokenMock, TestToken, UUPSProxiableMock__factory, @@ -306,6 +310,17 @@ export default class TestEnvironment { "InstantDistributionAgreementV1", this.sf.agreements.ida.address )), + (this.contracts.gda = await ethers.getContractAt( + "GeneralDistributionAgreementV1", + await this.contracts.superfluid.getAgreementClass( + ethers.utils.solidityKeccak256( + ["string"], + [ + "org.superfluid-finance.agreements.GeneralDistributionAgreement.v1", + ] + ) + ) + )), // load governance contract (this.contracts.governance = await ethers.getContractAt( "TestGovernance", @@ -571,9 +586,14 @@ export default class TestEnvironment { deployNFTContracts = async () => { let constantOutflowNFT; - let constantInflowNFTProxy; + let constantInflowNFT; let cofNFTLogicAddress; let cifNFTLogicAddress; + let paNFTLogicAddress; + let poolAdminNFT; + let pmNFTLogicAddress; + let poolMemberNFT; + const superTokenFactoryLogicAddress = await this.contracts.superfluid.getSuperTokenFactoryLogic(); const superTokenFactory = await ethers.getContractAt( @@ -591,13 +611,22 @@ export default class TestEnvironment { const constantInflowNFTProxyAddress = await superTokenLogic.CONSTANT_INFLOW_NFT(); + const poolAdminNFTProxyAddress = await superTokenLogic.POOL_ADMIN_NFT(); + const poolMemberNFTProxyAddress = + await superTokenLogic.POOL_MEMBER_NFT(); + if ( constantOutflowNFTProxyAddress === ethers.constants.AddressZero || - constantInflowNFTProxyAddress === ethers.constants.AddressZero + constantInflowNFTProxyAddress === ethers.constants.AddressZero || + poolAdminNFTProxyAddress === ethers.constants.AddressZero || + poolMemberNFTProxyAddress === ethers.constants.AddressZero ) { const cofProxy = await this.deployContract("UUPSProxy"); const cifProxy = await this.deployContract("UUPSProxy"); + const paProxy = await this.deployContract("UUPSProxy"); + const pmProxy = await this.deployContract("UUPSProxy"); + const constantOutflowNFTLogic = await this.deployContract( "ConstantOutflowNFT", @@ -605,6 +634,7 @@ export default class TestEnvironment { cifProxy.address ); cofNFTLogicAddress = constantOutflowNFTLogic.address; + const constantInflowNFTLogic = await this.deployContract( "ConstantInflowNFT", @@ -612,6 +642,19 @@ export default class TestEnvironment { cofProxy.address ); cifNFTLogicAddress = constantInflowNFTLogic.address; + + const poolAdminNFTLogic = await this.deployContract( + "PoolAdminNFT", + this.contracts.superfluid.address + ); + paNFTLogicAddress = poolAdminNFTLogic.address; + + const poolMemberNFTLogic = await this.deployContract( + "PoolMemberNFT", + this.contracts.superfluid.address + ); + pmNFTLogicAddress = poolMemberNFTLogic.address; + const signer = await ethers.getSigner(this.aliases.admin); const proxiableCofLogic = UUPSProxiableMock__factory.connect( constantOutflowNFTLogic.address, @@ -621,37 +664,71 @@ export default class TestEnvironment { constantInflowNFTLogic.address, signer ); + const proxiablePaLogic = UUPSProxiableMock__factory.connect( + poolAdminNFTLogic.address, + signer + ); + const proxiablePmLogic = UUPSProxiableMock__factory.connect( + poolMemberNFTLogic.address, + signer + ); await proxiableCofLogic.castrate(); await proxiableCifLogic.castrate(); + await proxiablePaLogic.castrate(); + await proxiablePmLogic.castrate(); await cofProxy.initializeProxy(constantOutflowNFTLogic.address); await cifProxy.initializeProxy(constantInflowNFTLogic.address); + await paProxy.initializeProxy(poolAdminNFTLogic.address); + await pmProxy.initializeProxy(poolMemberNFTLogic.address); constantOutflowNFT = ConstantOutflowNFT__factory.connect( cofProxy.address, signer ); - constantInflowNFTProxy = ConstantInflowNFT__factory.connect( + constantInflowNFT = ConstantInflowNFT__factory.connect( cifProxy.address, signer ); + poolAdminNFT = PoolAdminNFT__factory.connect( + paProxy.address, + signer + ); + poolMemberNFT = PoolMemberNFT__factory.connect( + pmProxy.address, + signer + ); } else { constantOutflowNFT = ConstantOutflowNFT__factory.connect( constantOutflowNFTProxyAddress, await ethers.getSigner(this.aliases.admin) ); - constantInflowNFTProxy = ConstantInflowNFT__factory.connect( + constantInflowNFT = ConstantInflowNFT__factory.connect( constantInflowNFTProxyAddress, await ethers.getSigner(this.aliases.admin) ); + poolAdminNFT = PoolAdminNFT__factory.connect( + poolAdminNFTProxyAddress, + await ethers.getSigner(this.aliases.admin) + ); + poolMemberNFT = PoolMemberNFT__factory.connect( + poolMemberNFTProxyAddress, + await ethers.getSigner(this.aliases.admin) + ); cofNFTLogicAddress = await constantOutflowNFT.getCodeAddress(); - cifNFTLogicAddress = await constantInflowNFTProxy.getCodeAddress(); + cifNFTLogicAddress = await constantInflowNFT.getCodeAddress(); + paNFTLogicAddress = await poolAdminNFT.getCodeAddress(); + pmNFTLogicAddress = await poolMemberNFT.getCodeAddress(); } return { constantOutflowNFTProxy: constantOutflowNFT, - constantInflowNFTProxy, + constantInflowNFTProxy: constantInflowNFT, cofNFTLogicAddress, cifNFTLogicAddress, + poolAdminNFTProxy: poolAdminNFT, + poolMemberNFTProxy: poolMemberNFT, + paNFTLogicAddress, + pmNFTLogicAddress, }; }; diff --git a/packages/ethereum-contracts/test/contracts/apps/SuperTokenV1Library.CFA.test.ts b/packages/ethereum-contracts/test/contracts/apps/SuperTokenV1Library.CFA.test.ts index 482fa6b69b..2d34e6a453 100644 --- a/packages/ethereum-contracts/test/contracts/apps/SuperTokenV1Library.CFA.test.ts +++ b/packages/ethereum-contracts/test/contracts/apps/SuperTokenV1Library.CFA.test.ts @@ -41,14 +41,20 @@ const callbackFunctionIndex = { export const deploySuperTokenAndNFTContractsAndInitialize = async ( t: TestEnvironment ) => { - const {constantOutflowNFTProxy, constantInflowNFTProxy} = - await t.deployNFTContracts(); + const { + constantOutflowNFTProxy, + constantInflowNFTProxy, + poolAdminNFTProxy, + poolMemberNFTProxy, + } = await t.deployNFTContracts(); const superToken = await t.deployContract( "SuperTokenMock", t.contracts.superfluid.address, "69", constantOutflowNFTProxy.address, - constantInflowNFTProxy.address + constantInflowNFTProxy.address, + poolAdminNFTProxy.address, + poolMemberNFTProxy.address ); return superToken; diff --git a/packages/ethereum-contracts/test/contracts/apps/SuperTokenV1Library.GDA.test.ts b/packages/ethereum-contracts/test/contracts/apps/SuperTokenV1Library.GDA.test.ts new file mode 100644 index 0000000000..68ddfff3e3 --- /dev/null +++ b/packages/ethereum-contracts/test/contracts/apps/SuperTokenV1Library.GDA.test.ts @@ -0,0 +1,446 @@ +import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; +import {ContractReceipt} from "ethers"; +import {ethers, expect, web3} from "hardhat"; + +import { + ConstantFlowAgreementV1, + SuperfluidMock, + SuperfluidPool, + SuperTokenLibraryGDAMock, + SuperTokenLibraryGDASuperAppMock, + SuperTokenMock, +} from "../../../typechain-types"; +import TestEnvironment from "../../TestEnvironment"; +import {toBN} from "../utils/helpers"; + +import {deploySuperTokenAndNFTContractsAndInitialize} from "./SuperTokenV1Library.CFA.test"; + +const mintAmount = "1000000000000000000000000000"; // a small loan of a billion dollars +const flowRate = "1000000000000"; + +const callbackFunctionIndex = { + UPDATE_MEMBER_UNITS: 0, + CONNECT_POOL: 1, + DISCONNECT_POOL: 2, + CLAIM_ALL: 3, + DISTRIBUTE: 4, + DISTRIBUTE_FLOW: 5, +}; + +const userData = ( + functionIndex: number, + pool = ethers.constants.AddressZero, + member = ethers.constants.AddressZero, + from = ethers.constants.AddressZero, + units = 0, + requestedAmount = 0, + requestedFlowRate = 0 +) => + web3.eth.abi.encodeParameters( + [ + "uint8", + "address", + "address", + "address", + "uint128", + "uint256", + "int96", + ], + [ + functionIndex, + pool, + member, + from, + units, + requestedAmount, + requestedFlowRate, + ] + ); + +describe("SuperTokenV1Library.GDA", function () { + this.timeout(300e3); + const t = TestEnvironment.getSingleton(); + let host: SuperfluidMock, cfa: ConstantFlowAgreementV1; + let aliceSigner: SignerWithAddress; + let createFlowCalldata: string; + let superTokenLibGDASuperAppMock: SuperTokenLibraryGDASuperAppMock; + + const getPoolAddressFromReceipt = (receipt: ContractReceipt) => { + const POOL_CREATED_TOPIC = ethers.utils.solidityKeccak256( + ["string"], + ["PoolCreated(address,address,address)"] + ); + const event = receipt.events?.find((x) => + x.topics.includes(POOL_CREATED_TOPIC) + ); + return ethers.utils.hexStripZeros( + event ? event.data : ethers.constants.AddressZero + ); + }; + + let alice: string, bob: string; + let superTokenLibraryGDAMock: SuperTokenLibraryGDAMock; + let superToken: SuperTokenMock; + + before(async () => { + await t.beforeTestSuite({ + isTruffle: true, + nAccounts: 5, + }); + ({alice, bob} = t.aliases); + superToken = t.tokens.SuperToken; + + cfa = t.contracts.cfa; + host = t.contracts.superfluid; + aliceSigner = await ethers.getSigner(alice); + }); + + beforeEach(async function () { + await t.beforeEachTestCase(); + const mockFactory = await ethers.getContractFactory( + "SuperTokenLibraryGDAMock" + ); + superTokenLibraryGDAMock = await mockFactory.deploy(); + + t.beforeEachTestCaseBenchmark(this); + }); + + this.afterEach(() => { + t.afterEachTestCaseBenchmark(); + }); + + it("#1.1 Should be able to create pool", async () => { + const createPoolTxn = await superTokenLibraryGDAMock.createPoolTest( + superToken.address, + alice, + { + transferabilityForUnitsOwner: true, + distributionFromAnyAddress: true, + } + ); + const receipt = await createPoolTxn.wait(); + const poolAddress = getPoolAddressFromReceipt(receipt); + const poolContract = await ethers.getContractAt( + "SuperfluidPool", + poolAddress + ); + expect(await poolContract.admin()).to.equal(alice); + expect(await poolContract.superToken()).to.equal(superToken.address); + }); + + context("With a pool", () => { + let pool: SuperfluidPool; + let admin: SignerWithAddress; + + beforeEach(async () => { + admin = await ethers.getSigner(alice); + const createPoolTxn = await superTokenLibraryGDAMock.createPoolTest( + superToken.address, + admin.address, + { + transferabilityForUnitsOwner: true, + distributionFromAnyAddress: true, + } + ); + const receipt = await createPoolTxn.wait(); + const poolAddress = getPoolAddressFromReceipt(receipt); + pool = await ethers.getContractAt("SuperfluidPool", poolAddress); + + // transfer tokens to the mock gda contract + await t.upgradeBalance("alice", t.configs.INIT_BALANCE); + await superToken + .connect(admin) + .transfer( + superTokenLibraryGDAMock.address, + t.configs.INIT_BALANCE + ); + }); + + it("#1.2 Should be able to distribute to pool", async () => { + expect(await pool.getUnits(bob)).to.equal("0"); + await pool.connect(admin).updateMemberUnits(bob, "10"); + expect(await pool.getUnits(bob)).to.equal("10"); + const bobBalanceBefore = await superToken.balanceOf(bob); + const requestedDistributionAmount = "100"; + const estimatedDistributionActualAmount = + await superTokenLibraryGDAMock.estimateDistributionActualAmountTest( + superToken.address, + superTokenLibraryGDAMock.address, + pool.address, + requestedDistributionAmount + ); + await superTokenLibraryGDAMock.distributeToPoolTest( + superToken.address, + superTokenLibraryGDAMock.address, + pool.address, + requestedDistributionAmount + ); + const bobBalanceAfter = await superToken.balanceOf(bob); + expect( + bobBalanceAfter, + bobBalanceBefore + .add(toBN(requestedDistributionAmount)) + .toString() + ); + expect( + estimatedDistributionActualAmount, + requestedDistributionAmount + ); + }); + + it("#1.3 Should be able to distribute flow to pool", async () => { + expect(await pool.getUnits(bob)).to.equal("0"); + await pool.connect(admin).updateMemberUnits(bob, "10"); + expect(await pool.getUnits(bob)).to.equal("10"); + const requestedFlowRate = "99"; + const estimatedFlowDistributionActualFlowRate = + await superTokenLibraryGDAMock.estimateFlowDistributionActualFlowRateTest( + superToken.address, + superTokenLibraryGDAMock.address, + pool.address, + requestedFlowRate + ); + await superTokenLibraryGDAMock.distributeFlowTest( + superToken.address, + superTokenLibraryGDAMock.address, + pool.address, + requestedFlowRate + ); + const flowDistributionFlowRate = + await superTokenLibraryGDAMock.getFlowDistributionFlowRateTest( + superToken.address, + superTokenLibraryGDAMock.address, + pool.address + ); + expect( + flowDistributionFlowRate.toString(), + estimatedFlowDistributionActualFlowRate.actualFlowRate.toString() + ); + }); + + it("#1.4 Should be able to connect to a pool", async () => { + await superTokenLibraryGDAMock.connectPoolTest( + superToken.address, + pool.address + ); + expect( + await superTokenLibraryGDAMock.isMemberConnectedTest( + superToken.address, + pool.address, + superTokenLibraryGDAMock.address + ) + ).to.equal(true); + }); + + it("#1.4 Should be able to disconnect from a pool", async () => { + await superTokenLibraryGDAMock.connectPoolTest( + superToken.address, + pool.address + ); + expect( + await superTokenLibraryGDAMock.isMemberConnectedTest( + superToken.address, + pool.address, + superTokenLibraryGDAMock.address + ) + ).to.equal(true); + await superTokenLibraryGDAMock.disconnectPoolTest( + superToken.address, + pool.address + ); + expect( + await superTokenLibraryGDAMock.isMemberConnectedTest( + superToken.address, + pool.address, + superTokenLibraryGDAMock.address + ) + ).to.equal(false); + }); + + context("#2 - Callback GDA Operations", async function () { + let appCreatedPool: SuperfluidPool; + let appSuperToken: SuperTokenMock; + + beforeEach(async () => { + appSuperToken = + await deploySuperTokenAndNFTContractsAndInitialize(t); + + await appSuperToken.mintInternal(alice, mintAmount, "0x", "0x"); + + const superTokenLibGDASuperAppMockFactory = + await ethers.getContractFactory( + "SuperTokenLibraryGDASuperAppMock" + ); + superTokenLibGDASuperAppMock = + await superTokenLibGDASuperAppMockFactory.deploy( + host.address + ); + + const createPoolTxn = + await superTokenLibGDASuperAppMock.createPoolTest( + appSuperToken.address, + superTokenLibGDASuperAppMock.address, + { + transferabilityForUnitsOwner: true, + distributionFromAnyAddress: true, + } + ); + const receipt = await createPoolTxn.wait(); + const poolAddress = getPoolAddressFromReceipt(receipt); + appCreatedPool = await ethers.getContractAt( + "SuperfluidPool", + poolAddress + ); + createFlowCalldata = + t.agreementHelper.cfaInterface.encodeFunctionData( + "createFlow", + [ + appSuperToken.address, + superTokenLibGDASuperAppMock.address, + flowRate, + "0x", + ] + ); + }); + + it("#2.1 should updateMemberUnits in callback", async () => { + await host + .connect(aliceSigner) + .callAgreement( + cfa.address, + createFlowCalldata, + userData( + callbackFunctionIndex.UPDATE_MEMBER_UNITS, + appCreatedPool.address, + bob, + ethers.constants.AddressZero, + 10 + ) + ); + expect(await appCreatedPool.getUnits(bob)).to.equal("10"); + }); + + it("#2.2 should connectPool in callback", async () => { + await host + .connect(aliceSigner) + .callAgreement( + cfa.address, + createFlowCalldata, + userData( + callbackFunctionIndex.CONNECT_POOL, + appCreatedPool.address + ) + ); + expect( + await t.contracts.gda["isMemberConnected(address,address)"]( + appCreatedPool.address, + superTokenLibGDASuperAppMock.address + ) + ).to.equal(true); + }); + + it("#2.3 should call disconnectPool in callback without revert", async () => { + await host + .connect(aliceSigner) + .callAgreement( + cfa.address, + createFlowCalldata, + userData( + callbackFunctionIndex.DISCONNECT_POOL, + appCreatedPool.address + ) + ); + }); + + it("#2.4 should claimAll in callback", async () => { + await expect( + host + .connect(aliceSigner) + .callAgreement( + cfa.address, + createFlowCalldata, + userData( + callbackFunctionIndex.CLAIM_ALL, + appCreatedPool.address, + bob + ) + ) + ) + .to.emit(appCreatedPool, "DistributionClaimed") + .withArgs(appSuperToken.address, bob, 0, 0); + }); + + it("#2.5 should distribute in callback", async () => { + await expect( + host + .connect(aliceSigner) + .callAgreement( + cfa.address, + createFlowCalldata, + userData( + callbackFunctionIndex.DISTRIBUTE, + appCreatedPool.address, + ethers.constants.AddressZero, + superTokenLibGDASuperAppMock.address, + 0, + 100 + ) + ) + ) + .to.emit(t.contracts.gda, "InstantDistributionUpdated") + .withArgs( + ethers.utils.getAddress(appSuperToken.address), + ethers.utils.getAddress(appCreatedPool.address), + ethers.utils.getAddress( + superTokenLibGDASuperAppMock.address + ), + ethers.utils.getAddress( + superTokenLibGDASuperAppMock.address + ), + 100, + 0, + "0x3078" + ); + }); + + it("#2.6 should distributeFlow in callback", async () => { + await expect( + host + .connect(aliceSigner) + .callAgreement( + cfa.address, + createFlowCalldata, + userData( + callbackFunctionIndex.DISTRIBUTE_FLOW, + appCreatedPool.address, + ethers.constants.AddressZero, + superTokenLibGDASuperAppMock.address, + 0, + 0, + 100 + ) + ) + ) + .to.emit(t.contracts.gda, "FlowDistributionUpdated") + .withArgs( + ethers.utils.getAddress(appSuperToken.address), + ethers.utils.getAddress(appCreatedPool.address), + ethers.utils.getAddress( + superTokenLibGDASuperAppMock.address + ), + ethers.utils.getAddress( + superTokenLibGDASuperAppMock.address + ), + 0, + 0, + 0, + ethers.utils.getAddress( + superTokenLibGDASuperAppMock.address + ), + 0, + "0x3078" + ); + }); + }); + }); +}); diff --git a/packages/ethereum-contracts/test/contracts/gov/SuperfluidGovernanceII.test.ts b/packages/ethereum-contracts/test/contracts/gov/SuperfluidGovernanceII.test.ts index 28f9f974d8..8e06678697 100644 --- a/packages/ethereum-contracts/test/contracts/gov/SuperfluidGovernanceII.test.ts +++ b/packages/ethereum-contracts/test/contracts/gov/SuperfluidGovernanceII.test.ts @@ -79,6 +79,7 @@ describe("Superfluid Ownable Governance Contract", function () { superfluid.address, ZERO_ADDRESS, [], + ZERO_ADDRESS, ZERO_ADDRESS ), governance, diff --git a/packages/ethereum-contracts/test/contracts/superfluid/SuperToken.NonStandard.test.ts b/packages/ethereum-contracts/test/contracts/superfluid/SuperToken.NonStandard.test.ts index 7f03b825aa..bbbeb74be4 100644 --- a/packages/ethereum-contracts/test/contracts/superfluid/SuperToken.NonStandard.test.ts +++ b/packages/ethereum-contracts/test/contracts/superfluid/SuperToken.NonStandard.test.ts @@ -67,14 +67,20 @@ describe("SuperToken's Non Standard Functions", function () { describe("#1 upgradability", () => { it("#1.1 storage layout", async () => { - const {constantOutflowNFTProxy, constantInflowNFTProxy} = - await t.deployNFTContracts(); + const { + constantOutflowNFTProxy, + constantInflowNFTProxy, + poolAdminNFTProxy, + poolMemberNFTProxy, + } = await t.deployNFTContracts(); const superTokenLogic = await t.deployContract( "SuperTokenStorageLayoutTester", superfluid.address, constantOutflowNFTProxy.address, - constantInflowNFTProxy.address + constantInflowNFTProxy.address, + poolAdminNFTProxy.address, + poolMemberNFTProxy.address ); await superTokenLogic.validateStorageLayout(); }); @@ -704,14 +710,20 @@ describe("SuperToken's Non Standard Functions", function () { }); it("#3.1 Custom token storage should not overlap with super token", async () => { - const {constantOutflowNFTProxy, constantInflowNFTProxy} = - await t.deployNFTContracts(); + const { + constantOutflowNFTProxy, + constantInflowNFTProxy, + poolAdminNFTProxy, + poolMemberNFTProxy, + } = await t.deployNFTContracts(); const superTokenLogic = await t.deployContract( "SuperTokenStorageLayoutTester", superfluid.address, constantOutflowNFTProxy.address, - constantInflowNFTProxy.address + constantInflowNFTProxy.address, + poolAdminNFTProxy.address, + poolMemberNFTProxy.address ); const a = await superTokenLogic.getLastSuperTokenStorageSlot(); const b = await customToken.getFirstCustomTokenStorageSlot(); diff --git a/packages/ethereum-contracts/test/contracts/superfluid/SuperTokenFactory.test.ts b/packages/ethereum-contracts/test/contracts/superfluid/SuperTokenFactory.test.ts index bda66083ff..0e19b464c4 100644 --- a/packages/ethereum-contracts/test/contracts/superfluid/SuperTokenFactory.test.ts +++ b/packages/ethereum-contracts/test/contracts/superfluid/SuperTokenFactory.test.ts @@ -70,13 +70,19 @@ describe("SuperTokenFactory Contract", function () { constantInflowNFTProxy, cofNFTLogicAddress, cifNFTLogicAddress, + poolAdminNFTProxy, + poolMemberNFTProxy, + paNFTLogicAddress, + pmNFTLogicAddress, } = await t.deployNFTContracts(); const superTokenLogic = await t.deployContract( "SuperTokenMock", superfluid.address, "0", constantOutflowNFTProxy.address, - constantInflowNFTProxy.address + constantInflowNFTProxy.address, + poolAdminNFTProxy.address, + poolMemberNFTProxy.address ); const tester = await t.deployContract( @@ -84,7 +90,9 @@ describe("SuperTokenFactory Contract", function () { superfluid.address, superTokenLogic.address, cofNFTLogicAddress, - cifNFTLogicAddress + cifNFTLogicAddress, + paNFTLogicAddress, + pmNFTLogicAddress ); await tester.validateStorageLayout(); }); @@ -149,13 +157,19 @@ describe("SuperTokenFactory Contract", function () { constantInflowNFTProxy, cofNFTLogicAddress, cifNFTLogicAddress, + poolAdminNFTProxy, + poolMemberNFTProxy, + paNFTLogicAddress, + pmNFTLogicAddress, } = await t.deployNFTContracts(); const superTokenLogic = await t.deployContract( "SuperTokenMock", superfluid.address, 42, constantOutflowNFTProxy.address, - constantInflowNFTProxy.address + constantInflowNFTProxy.address, + poolAdminNFTProxy.address, + poolMemberNFTProxy.address ); const factory2Logic = await t.deployContract( @@ -163,13 +177,16 @@ describe("SuperTokenFactory Contract", function () { superfluid.address, superTokenLogic.address, cofNFTLogicAddress, - cifNFTLogicAddress + cifNFTLogicAddress, + paNFTLogicAddress, + pmNFTLogicAddress ); await governance.updateContracts( superfluid.address, ZERO_ADDRESS, [], - factory2Logic.address + factory2Logic.address, + ZERO_ADDRESS ); await superfluid.getSuperTokenFactoryLogic(); } @@ -270,14 +287,20 @@ describe("SuperTokenFactory Contract", function () { await updateSuperTokenFactory(); assert.equal((await superToken1.waterMark()).toString(), "0"); - const {constantOutflowNFTProxy, constantInflowNFTProxy} = - await t.deployNFTContracts(); + const { + constantOutflowNFTProxy, + constantInflowNFTProxy, + poolAdminNFTProxy, + poolMemberNFTProxy, + } = await t.deployNFTContracts(); const superTokenLogic = await t.deployContract( "SuperTokenMock", superfluid.address, 69, constantOutflowNFTProxy.address, - constantInflowNFTProxy.address + constantInflowNFTProxy.address, + poolAdminNFTProxy.address, + poolMemberNFTProxy.address ); await governance[ @@ -298,12 +321,18 @@ describe("SuperTokenFactory Contract", function () { constantInflowNFTProxy, cofNFTLogicAddress, cifNFTLogicAddress, + poolAdminNFTProxy, + poolMemberNFTProxy, + paNFTLogicAddress, + pmNFTLogicAddress, } = await t.deployNFTContracts(); const superTokenLogic = await t.deployContract( "SuperToken", superfluid.address, constantOutflowNFTProxy.address, - constantInflowNFTProxy.address + constantInflowNFTProxy.address, + poolAdminNFTProxy.address, + poolMemberNFTProxy.address ); const factory2Logic = await t.deployContract( @@ -311,13 +340,16 @@ describe("SuperTokenFactory Contract", function () { superfluid.address, superTokenLogic.address, cofNFTLogicAddress, - cifNFTLogicAddress + cifNFTLogicAddress, + paNFTLogicAddress, + pmNFTLogicAddress ); await governance.updateContracts( superfluid.address, ZERO_ADDRESS, [], - factory2Logic.address + factory2Logic.address, + ZERO_ADDRESS ); await expectCustomError( diff --git a/packages/ethereum-contracts/test/contracts/superfluid/Superfluid.test.ts b/packages/ethereum-contracts/test/contracts/superfluid/Superfluid.test.ts index a3c55df496..98fe04baf7 100644 --- a/packages/ethereum-contracts/test/contracts/superfluid/Superfluid.test.ts +++ b/packages/ethereum-contracts/test/contracts/superfluid/Superfluid.test.ts @@ -122,6 +122,7 @@ describe("Superfluid Host Contract", function () { superfluid.address, mock1.address, [], + ZERO_ADDRESS, ZERO_ADDRESS ); @@ -137,6 +138,7 @@ describe("Superfluid Host Contract", function () { superfluid.address, mock2.address, [], + ZERO_ADDRESS, ZERO_ADDRESS ), superfluid, @@ -240,6 +242,7 @@ describe("Superfluid Host Contract", function () { superfluid.address, ZERO_ADDRESS, [mockA2.address], + ZERO_ADDRESS, ZERO_ADDRESS ); console.debug( @@ -349,6 +352,7 @@ describe("Superfluid Host Contract", function () { superfluid.address, ZERO_ADDRESS, [mockA.address], + ZERO_ADDRESS, ZERO_ADDRESS ), superfluid, @@ -403,12 +407,18 @@ describe("Superfluid Host Contract", function () { constantInflowNFTProxy, cofNFTLogicAddress, cifNFTLogicAddress, + poolAdminNFTProxy, + poolMemberNFTProxy, + paNFTLogicAddress, + pmNFTLogicAddress, } = await t.deployNFTContracts(); const superTokenLogic = await t.deployContract( "SuperToken", superfluid.address, constantOutflowNFTProxy.address, - constantInflowNFTProxy.address + constantInflowNFTProxy.address, + poolAdminNFTProxy.address, + poolMemberNFTProxy.address ); const factory2LogicFactory = await ethers.getContractFactory("SuperTokenFactory"); @@ -416,13 +426,16 @@ describe("Superfluid Host Contract", function () { superfluid.address, superTokenLogic.address, cofNFTLogicAddress, - cifNFTLogicAddress + cifNFTLogicAddress, + paNFTLogicAddress, + pmNFTLogicAddress ); await governance.updateContracts( superfluid.address, ZERO_ADDRESS, [], - factory2Logic.address + factory2Logic.address, + ZERO_ADDRESS ); assert.equal( await superfluid.getSuperTokenFactory(), @@ -443,12 +456,18 @@ describe("Superfluid Host Contract", function () { constantInflowNFTProxy, cofNFTLogicAddress, cifNFTLogicAddress, + poolAdminNFTProxy, + poolMemberNFTProxy, + paNFTLogicAddress, + pmNFTLogicAddress, } = await t.deployNFTContracts(); const superTokenLogic = await t.deployContract( "SuperToken", superfluid.address, constantOutflowNFTProxy.address, - constantInflowNFTProxy.address + constantInflowNFTProxy.address, + poolAdminNFTProxy.address, + poolMemberNFTProxy.address ); const factory2LogicFactory = await ethers.getContractFactory( "SuperTokenFactoryUpdateLogicContractsTester" @@ -457,13 +476,16 @@ describe("Superfluid Host Contract", function () { superfluid.address, superTokenLogic.address, cofNFTLogicAddress, - cifNFTLogicAddress + cifNFTLogicAddress, + paNFTLogicAddress, + pmNFTLogicAddress ); await governance.updateContracts( superfluid.address, ZERO_ADDRESS, [], - factory2Logic.address + factory2Logic.address, + ZERO_ADDRESS ); assert.equal( await superfluid.getSuperTokenFactory(), @@ -2617,6 +2639,7 @@ describe("Superfluid Host Contract", function () { superfluid.address, ZERO_ADDRESS, [t.contracts.ida.address], + ZERO_ADDRESS, ZERO_ADDRESS ), superfluid, @@ -2634,26 +2657,35 @@ describe("Superfluid Host Contract", function () { constantInflowNFTProxy, cofNFTLogicAddress, cifNFTLogicAddress, + poolAdminNFTProxy, + poolMemberNFTProxy, + paNFTLogicAddress, + pmNFTLogicAddress, } = await t.deployNFTContracts(); const superTokenLogic = await t.deployContract( "SuperToken", superfluid.address, constantOutflowNFTProxy.address, - constantInflowNFTProxy.address + constantInflowNFTProxy.address, + poolAdminNFTProxy.address, + poolMemberNFTProxy.address ); const factory2Logic = await t.deployContract( "SuperTokenFactory", superfluid.address, superTokenLogic.address, cofNFTLogicAddress, - cifNFTLogicAddress + cifNFTLogicAddress, + paNFTLogicAddress, + pmNFTLogicAddress ); await expectCustomError( governance.updateContracts( superfluid.address, ZERO_ADDRESS, [], - factory2Logic.address + factory2Logic.address, + ZERO_ADDRESS ), superfluid, "HOST_NON_UPGRADEABLE" @@ -2672,6 +2704,7 @@ describe("Superfluid Host Contract", function () { superfluid.address, mock1.address, [], + ZERO_ADDRESS, ZERO_ADDRESS ), superfluid, diff --git a/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.sol b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.sol index 6c4e805903..e31dedfeb2 100644 --- a/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.sol +++ b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.sol @@ -5,9 +5,21 @@ import "forge-std/Test.sol"; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - import { ERC1820RegistryCompiled } from "../../contracts/libs/ERC1820RegistryCompiled.sol"; import { SuperfluidFrameworkDeployer } from "../../contracts/utils/SuperfluidFrameworkDeployer.sol"; +import { Superfluid } from "../../contracts/superfluid/Superfluid.sol"; +import { ISuperfluidPool, SuperfluidPool } from "../../contracts/agreements/gdav1/SuperfluidPool.sol"; +import { IFlowNFTBase } from "../../contracts/interfaces/superfluid/IFlowNFTBase.sol"; +import { + IGeneralDistributionAgreementV1, + PoolConfig +} from "../../contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; +import { IPoolNFTBase } from "../../contracts/interfaces/agreements/gdav1/IPoolNFTBase.sol"; +import { IPoolAdminNFT } from "../../contracts/interfaces/agreements/gdav1/IPoolAdminNFT.sol"; +import { IPoolMemberNFT } from "../../contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol"; +import { IConstantOutflowNFT } from "../../contracts/interfaces/superfluid/IConstantOutflowNFT.sol"; +import { IConstantInflowNFT } from "../../contracts/interfaces/superfluid/IConstantInflowNFT.sol"; +import { ISuperfluidToken } from "../../contracts/interfaces/superfluid/ISuperfluidToken.sol"; import { ISETH } from "../../contracts/interfaces/tokens/ISETH.sol"; import { UUPSProxy } from "../../contracts/upgradability/UUPSProxy.sol"; import { ConstantFlowAgreementV1 } from "../../contracts/agreements/ConstantFlowAgreementV1.sol"; @@ -35,6 +47,7 @@ import { TestToken } from "../../contracts/utils/TestToken.sol"; contract FoundrySuperfluidTester is Test { using SuperTokenV1Library for ISuperToken; using EnumerableSet for EnumerableSet.Bytes32Set; + using EnumerableSet for EnumerableSet.AddressSet; using SafeCast for uint256; using SafeCast for int256; @@ -47,6 +60,11 @@ contract FoundrySuperfluidTester is Test { UNSUPPORTED_TOKEN_TYPE } + struct _StackVars_UseBools { + bool useForwarder; + bool useGDA; + } + struct RealtimeBalance { int256 availableBalance; uint256 deposit; @@ -61,6 +79,34 @@ contract FoundrySuperfluidTester is Test { uint32 indexId; } + struct ExpectedSuperfluidPoolData { + int128 totalUnits; + int128 connectedUnits; + int128 disconnectedUnits; + int96 connectedFlowRate; + int96 disconnectedFlowRate; + int256 disconnectedBalance; + } + + struct ExpectedPoolMemberData { + bool isConnected; + uint128 ownedUnits; + int96 flowRate; + int96 netFlowRate; + } + + struct PoolUnitData { + uint128 totalUnits; + uint128 connectedUnits; + uint128 disconnectedUnits; + } + + struct PoolFlowRateData { + int96 totalFlowRate; + int96 totalConnectedFlowRate; + int96 totalDisconnectedFlowRate; + } + error INVALID_TEST_SUPER_TOKEN_TYPE(); SuperfluidFrameworkDeployer internal immutable sfDeployer; @@ -68,7 +114,7 @@ contract FoundrySuperfluidTester is Test { uint256 internal constant DEFAULT_WARP_TIME = 1 days; uint256 internal constant INIT_TOKEN_BALANCE = type(uint128).max; - uint256 internal constant INIT_SUPER_TOKEN_BALANCE = type(uint64).max; + uint256 internal constant INIT_SUPER_TOKEN_BALANCE = type(uint88).max; string internal constant DEFAULT_TEST_TOKEN_TYPE = "WRAPPER_SUPER_TOKEN"; string internal constant TOKEN_TYPE_ENV_KEY = "TOKEN_TYPE"; @@ -85,7 +131,7 @@ contract FoundrySuperfluidTester is Test { address[] internal TEST_ACCOUNTS = [admin, alice, bob, carol, dan, eve, frank, grace, heidi, ivan]; /// @dev Other account addresses added that aren't testers (pools, super apps, smart contracts) - address[] internal OTHER_ACCOUNTS; + EnumerableSet.AddressSet internal OTHER_ACCOUNTS; uint256 internal immutable N_TESTERS; @@ -116,6 +162,14 @@ contract FoundrySuperfluidTester is Test { /// @notice A mapping from super token to subId to sub.indexValue for the IDA mapping(ISuperToken => mapping(bytes32 subId => uint128 indexValue)) internal _lastUpdatedSubIndexValues; + /// @notice A mapping from pool to + mapping(address pool => EnumerableSet.AddressSet members) internal _poolMembers; + mapping(address pool => mapping(address member => ExpectedPoolMemberData expectedData)) internal + _poolToExpectedMemberData; + + /// @notice The default poolConfig (true, true) + PoolConfig public poolConfig; + constructor(uint8 nTesters) { // deploy ERC1820 registry vm.etch(ERC1820RegistryCompiled.at, ERC1820RegistryCompiled.bin); @@ -140,12 +194,16 @@ contract FoundrySuperfluidTester is Test { require(nTesters <= TEST_ACCOUNTS.length, "too many testers"); N_TESTERS = nTesters; + _addAccount(address(sf.gda)); + // Set the token type being tested string memory tokenType = vm.envOr(TOKEN_TYPE_ENV_KEY, DEFAULT_TEST_TOKEN_TYPE); bytes32 hashedTokenType = keccak256(abi.encode(tokenType)); _addAccount(address(sf.toga)); + poolConfig = PoolConfig({ transferabilityForUnitsOwner: true, distributionFromAnyAddress: true }); + // @note we must use a ternary expression because immutable variables cannot be initialized // in an if statement testSuperTokenType = hashedTokenType == keccak256(abi.encode("WRAPPER_SUPER_TOKEN")) @@ -172,7 +230,7 @@ contract FoundrySuperfluidTester is Test { /// @notice Deploys a Wrapper SuperToken with an underlying test token and gives tokens to the test accounts function _setUpWrapperSuperToken() internal { - (token, superToken) = sfDeployer.deployWrapperSuperToken("FTT", "FTT", 18, type(uint256).max); + (token, superToken) = sfDeployer.deployWrapperSuperToken("FTT", "FTT", 18, type(uint256).max, address(0)); address[] memory accounts = _listAccounts(); for (uint256 i = 0; i < accounts.length; ++i) { @@ -192,6 +250,7 @@ contract FoundrySuperfluidTester is Test { /// @dev We use vm.deal to give each account a starting amount of ether function _setUpNativeAssetSuperToken() internal { (superToken) = sfDeployer.deployNativeAssetSuperToken("Super ETH", "ETHx"); + address[] memory accounts = _listAccounts(); for (uint256 i = 0; i < accounts.length; ++i) { address account = accounts[i]; @@ -200,7 +259,7 @@ contract FoundrySuperfluidTester is Test { ISETH(address(superToken)).upgradeByETH{ value: INIT_SUPER_TOKEN_BALANCE }(); _expectedTotalSupply += INIT_SUPER_TOKEN_BALANCE; vm.stopPrank(); - _helperTakeBalanceSnapshot(superToken, TEST_ACCOUNTS[i]); + _helperTakeBalanceSnapshot(superToken, account); } } @@ -212,6 +271,7 @@ contract FoundrySuperfluidTester is Test { uint256 initialSupply = INIT_SUPER_TOKEN_BALANCE * accounts.length; (superToken) = sfDeployer.deployPureSuperToken("Super MR", "MRx", initialSupply); _expectedTotalSupply = initialSupply; + for (uint256 i = 0; i < accounts.length; ++i) { address account = accounts[i]; superToken.transfer(account, INIT_SUPER_TOKEN_BALANCE); @@ -240,16 +300,22 @@ contract FoundrySuperfluidTester is Test { /// @notice Adds an account to the testing mix function _addAccount(address account) internal { - OTHER_ACCOUNTS.push(account); + if (OTHER_ACCOUNTS.contains(account)) return; + + for (uint i = 0; i < TEST_ACCOUNTS.length; ++i) { + if (TEST_ACCOUNTS[i] == account) return; + } + + OTHER_ACCOUNTS.add(account); } function _listAccounts() internal view returns (address[] memory accounts) { - accounts = new address[](N_TESTERS + OTHER_ACCOUNTS.length); + accounts = new address[](N_TESTERS + OTHER_ACCOUNTS.values().length); for (uint i = 0; i < N_TESTERS; ++i) { accounts[i] = address(TEST_ACCOUNTS[i]); } - for (uint i = 0; i < OTHER_ACCOUNTS.length; ++i) { - accounts[i + N_TESTERS] = OTHER_ACCOUNTS[i]; + for (uint i = 0; i < OTHER_ACCOUNTS.values().length; ++i) { + accounts[i + N_TESTERS] = OTHER_ACCOUNTS.values()[i]; } } @@ -297,20 +363,6 @@ contract FoundrySuperfluidTester is Test { return netFlowRateSum == 0; } - /// @notice Warps forwards 1 day and asserts balances of all testers and global invariants - function _warpAndAssertAll(ISuperToken superToken_) internal virtual { - vm.warp(block.timestamp + DEFAULT_WARP_TIME); - _assertRealTimeBalances(superToken_); - _assertGlobalInvariants(); - } - - /// @notice Warps forwards `time` seconds and asserts balances of all testers and global invariants - function _warpAndAssertAll(ISuperToken superToken_, uint256 time) internal virtual { - vm.warp(block.timestamp + time); - _assertRealTimeBalances(superToken_); - _assertGlobalInvariants(); - } - /// @notice Asserts that the global invariants hold true function _assertGlobalInvariants() internal virtual { _assertInvariantLiquiditySum(); @@ -335,6 +387,20 @@ contract FoundrySuperfluidTester is Test { assertTrue(_defintionAumGtEqSuperTokenTotalSupplyInvariant(), "Invariant: AUM > SuperToken Total Supply"); } + /// @notice Warps forwards 1 day and asserts balances of all testers and global invariants + function _warpAndAssertAll(ISuperToken superToken_) internal virtual { + vm.warp(block.timestamp + DEFAULT_WARP_TIME); + _assertRealTimeBalances(superToken_); + _assertGlobalInvariants(); + } + + /// @notice Warps forwards `time` seconds and asserts balances of all testers and global invariants + function _warpAndAssertAll(ISuperToken superToken_, uint256 time) internal virtual { + vm.warp(block.timestamp + time); + _assertRealTimeBalances(superToken_); + _assertGlobalInvariants(); + } + /*////////////////////////////////////////////////////////////////////////// Assume Helpers //////////////////////////////////////////////////////////////////////////*/ @@ -342,8 +408,8 @@ contract FoundrySuperfluidTester is Test { /// @dev Flow rate must be greater than 0 and less than or equal to int32.max function _assumeValidFlowRate(int96 desiredFlowRate) internal pure returns (int96 flowRate) { vm.assume(desiredFlowRate > 0); - vm.assume(desiredFlowRate <= int96(type(int32).max)); - flowRate = int96(int32(desiredFlowRate)); + vm.assume(desiredFlowRate <= int96(uint96(uint256(type(uint64).max)))); + flowRate = desiredFlowRate; } /*////////////////////////////////////////////////////////////////////////// @@ -398,7 +464,11 @@ contract FoundrySuperfluidTester is Test { (int256 availableBalance, uint256 deposit, uint256 owedDeposit,) = superToken_.realtimeBalanceOfNow(accounts[i]); - liquiditySum += availableBalance + int256(deposit) - int256(owedDeposit); + // FIXME: correct formula + // liquiditySum += availableBalance + int256(deposit) - int256(owedDeposit); + // current faulty one + liquiditySum += + availableBalance + (deposit > owedDeposit ? int256(deposit) - int256(owedDeposit) : int256(0)); } } @@ -494,7 +564,7 @@ contract FoundrySuperfluidTester is Test { assertTrue(netFlowRate < 0, "_helperWarpToCritical: netFlowRate must be less than 0 to reach critical"); assertTrue(secondsCritical > 0, "_helperWarpToCritical: secondsCritical must be > 0 to reach critical"); (int256 ab,,) = superToken_.realtimeBalanceOf(account, block.timestamp); - int256 timeToZero = ab / netFlowRate; + int256 timeToZero = ab / netFlowRate < 0 ? (ab / netFlowRate) * -1 : ab / netFlowRate; uint256 amountToWarp = timeToZero.toUint256() + secondsCritical; vm.warp(block.timestamp + amountToWarp); assertTrue(superToken_.isAccountCriticalNow(account), "_helperWarpToCritical: account is not critical"); @@ -515,7 +585,7 @@ contract FoundrySuperfluidTester is Test { assertTrue(netFlowRate < 0, "_helperWarpToCritical: netFlowRate must be less than 0 to reach critical"); assertTrue(secondsInsolvent > 0, "_helperWarpToInsolvency: secondsInsolvent must be > 0 to reach insolvency"); (int256 ab,,) = superToken_.realtimeBalanceOf(account, block.timestamp); - int256 timeToZero = ab / netFlowRate; + int256 timeToZero = ab / netFlowRate < 0 ? (ab / netFlowRate) * -1 : ab / netFlowRate; uint256 amountToWarp = timeToZero.toUint256() + liquidationPeriod + secondsInsolvent; vm.warp(block.timestamp + amountToWarp); assertFalse(superToken_.isAccountSolventNow(account), "_helperWarpToInsolvency: account is still solvent"); @@ -603,6 +673,14 @@ contract FoundrySuperfluidTester is Test { } // Write Helpers - SuperToken + function _helperTransferAll(ISuperToken superToken_, address sender, address receiver) internal { + vm.startPrank(sender); + superToken_.transferAll(receiver); + vm.stopPrank(); + + _helperTakeBalanceSnapshot(superToken_, sender); + _helperTakeBalanceSnapshot(superToken_, receiver); + } function _helperDeploySuperTokenAndInitialize( ISuperToken previousSuperToken, @@ -610,14 +688,16 @@ contract FoundrySuperfluidTester is Test { uint8 underlyingDecimals, string memory name, string memory symbol, - address admin + address _admin ) internal returns (SuperToken localSuperToken) { localSuperToken = new SuperToken( sf.host, previousSuperToken.CONSTANT_OUTFLOW_NFT(), - previousSuperToken.CONSTANT_INFLOW_NFT() + previousSuperToken.CONSTANT_INFLOW_NFT(), + previousSuperToken.POOL_ADMIN_NFT(), + previousSuperToken.POOL_MEMBER_NFT() ); - localSuperToken.initializeWithAdmin(underlyingToken, underlyingDecimals, name, symbol, admin); + localSuperToken.initializeWithAdmin(underlyingToken, underlyingDecimals, name, symbol, _admin); } // Write Helpers - ConstantFlowAgreementV1 @@ -664,8 +744,6 @@ contract FoundrySuperfluidTester is Test { // Assert RTB for all users _assertRealTimeBalances(superToken_); - - // Assert Global Invariants _assertGlobalInvariants(); } @@ -710,8 +788,6 @@ contract FoundrySuperfluidTester is Test { // Assert RTB for all users _assertRealTimeBalances(superToken_); - - // Assert Global Invariants _assertGlobalInvariants(); } @@ -743,6 +819,15 @@ contract FoundrySuperfluidTester is Test { _helperTakeBalanceSnapshot(superToken_, sender); _helperTakeBalanceSnapshot(superToken_, receiver); + + if (caller != sender && caller != receiver) { + _helperTakeBalanceSnapshot(superToken_, caller); + } + + // Get the default reward address for the token and update their snapshot too in the + // liquidation case + address rewardAddress = sf.governance.getRewardAddress(sf.host, superToken_); + _helperTakeBalanceSnapshot(superToken_, rewardAddress); } // Assert Flow Data + Account Flow Info for sender/receiver @@ -755,8 +840,6 @@ contract FoundrySuperfluidTester is Test { // Assert RTB for all users _assertRealTimeBalances(superToken_); - - // Assert Global Invariants _assertGlobalInvariants(); } @@ -828,8 +911,6 @@ contract FoundrySuperfluidTester is Test { // Assert RTB for all users _assertRealTimeBalances(superToken_); - - // Assert Global Invariants _assertGlobalInvariants(); } @@ -899,6 +980,7 @@ contract FoundrySuperfluidTester is Test { // Assert RTB for all users _assertRealTimeBalances(superToken_); + _assertGlobalInvariants(); // Assert Global Invariants _assertGlobalInvariants(); @@ -960,8 +1042,6 @@ contract FoundrySuperfluidTester is Test { // Assert RTB for all users _assertRealTimeBalances(superToken_); - - // Assert Global Invariants _assertGlobalInvariants(); } @@ -1036,7 +1116,7 @@ contract FoundrySuperfluidTester is Test { _assertGlobalInvariants(); } - /// @notice Distributes tokens to subscribers + /// @notice Executes an IDA distribution of tokens to subscribers /// @dev We assert: /// - The index data has been updated as expected /// - the publisher's balance and deposit has been updated as expected @@ -1044,7 +1124,9 @@ contract FoundrySuperfluidTester is Test { /// @param publisher The publisher of the index /// @param indexId The indexId to update /// @param amount The new index value to update to - function _helperDistribute(ISuperToken superToken_, address publisher, uint32 indexId, uint256 amount) internal { + function _helperDistributeViaIDA(ISuperToken superToken_, address publisher, uint32 indexId, uint256 amount) + internal + { // Get Index Data and Publisher Balance Before (, uint128 indexValueBefore, uint128 totalUnitsApprovedBefore, uint128 totalUnitsPendingBefore) = superToken_.getIndex(publisher, indexId); @@ -1363,7 +1445,7 @@ contract FoundrySuperfluidTester is Test { /// @param publisher The publisher of the subscription /// @param indexId The index ID of the index /// @param subscriber The subscriber of the subscription - function _helperClaim( + function _helperClaimViaIDA( ISuperToken superToken_, address caller, address publisher, @@ -1414,6 +1496,616 @@ contract FoundrySuperfluidTester is Test { _assertGlobalInvariants(); } + // Write Helpers - GeneralDistributionAgreementV1/SuperfluidPool + + function _helperCreatePool( + ISuperToken _superToken, + address _caller, + address _poolAdmin, + bool _useForwarder, + PoolConfig memory _poolConfig + ) internal returns (ISuperfluidPool) { + ISuperfluidPool localPool; + + vm.startPrank(_caller); + if (!_useForwarder) { + localPool = SuperfluidPool(address(sf.gda.createPool(_superToken, _poolAdmin, _poolConfig))); + } else { + (, localPool) = sf.gdaV1Forwarder.createPool(_superToken, _poolAdmin, _poolConfig); + } + vm.stopPrank(); + _addAccount(address(localPool)); + + // Assert Pool Creation was properly handled + address poolAdmin = localPool.admin(); + { + bool isPool = _useForwarder + ? sf.gdaV1Forwarder.isPool(_superToken, address(localPool)) + : sf.gda.isPool(_superToken, address(localPool)); + assertTrue(isPool, "GDAv1.t: Created pool is not pool"); + assertEq(poolAdmin, _poolAdmin, "GDAv1.t: Pool admin is incorrect"); + assertEq(address(localPool.superToken()), address(_superToken), "GDAv1.t: Pool super token is incorrect"); + } + + IPoolAdminNFT poolAdminNft = SuperToken(address(_superToken)).POOL_ADMIN_NFT(); + uint256 tokenId = poolAdminNft.getTokenId(address(localPool), _poolAdmin); + + // Assert PoolAdminNFT Owner is expected + assertEq( + poolAdminNft.ownerOf(tokenId), _poolAdmin, "_helperCreatePool: Pool Admin NFT is not owned by pool admin" + ); + + // Assert PoolAdminNFTData is expected + { + IPoolAdminNFT.PoolAdminNFTData memory poolAdminData = poolAdminNft.poolAdminDataByTokenId(tokenId); + assertEq(poolAdminData.pool, address(localPool), "_helperCreatePool: Pool Admin NFT pool mismatch"); + assertEq(poolAdminData.admin, _poolAdmin, "_helperCreatePool: Pool Admin NFT admin mismatch"); + } + + // Assert Admin is PoolAdjustment Flow receiver + { + (address adjustmentFlowRecipient,,) = _useForwarder + ? sf.gdaV1Forwarder.getPoolAdjustmentFlowInfo(localPool) + : sf.gda.getPoolAdjustmentFlowInfo(localPool); + assertEq(poolAdmin, adjustmentFlowRecipient, "_helperCreatePool: Incorrect pool adjustment flow receiver"); + } + + return localPool; + } + + function _helperCreatePool(ISuperToken _superToken, address _caller, address _poolAdmin) + internal + returns (ISuperfluidPool) + { + return _helperCreatePool(_superToken, _caller, _poolAdmin, false, poolConfig); + } + + function _helperUpdateMemberUnits(ISuperfluidPool pool_, address caller_, address member_, uint128 newUnits_) + internal + { + _StackVars_UseBools memory useBools_; + _helperUpdateMemberUnits(pool_, caller_, member_, newUnits_, useBools_); + } + + function _updateMemberUnits( + ISuperfluidPool pool_, + ISuperToken poolSuperToken, + address caller_, + address member_, + uint128 newUnits_, + _StackVars_UseBools memory useBools_ + ) internal { + vm.startPrank(caller_); + if (useBools_.useGDA) { + if (useBools_.useForwarder) { + sf.gdaV1Forwarder.updateMemberUnits(pool_, member_, newUnits_, new bytes(0)); + } else { + poolSuperToken.updateMemberUnits(pool_, member_, newUnits_); + } + } else { + pool_.updateMemberUnits(member_, newUnits_); + } + vm.stopPrank(); + } + + function _helperUpdateMemberUnits( + ISuperfluidPool pool_, + address caller_, + address member_, + uint128 newUnits_, + _StackVars_UseBools memory useBools_ + ) internal { + // there is a hard restriction in which total units must never exceed type(int96).max + vm.assume(newUnits_ < type(uint72).max); + ISuperToken poolSuperToken = ISuperToken(address(pool_.superToken())); + if (caller_ == address(0) || member_ == address(0) || sf.gda.isPool(poolSuperToken, member_)) return; + + (bool isConnected, int256 oldUnits,) = _helperGetMemberPoolState(pool_, member_); + + PoolUnitData memory poolUnitDataBefore = _helperGetPoolUnitsData(pool_); + + (int256 claimableBalance,) = pool_.getClaimableNow(member_); + (int256 balanceBefore,,,) = poolSuperToken.realtimeBalanceOfNow(member_); + { + _updateMemberUnits(pool_, poolSuperToken, caller_, member_, newUnits_, useBools_); + } + PoolUnitData memory poolUnitDataAfter = _helperGetPoolUnitsData(pool_); + + { + _helperTakeBalanceSnapshot(ISuperToken(address(poolSuperToken)), member_); + } + + assertEq(pool_.getUnits(member_), newUnits_, "GDAv1.t: Members' units incorrectly set"); + + // Assert that pending balance is claimed if user is disconnected + if (!isConnected) { + (int256 balanceAfter,,,) = poolSuperToken.realtimeBalanceOfNow(member_); + assertEq( + balanceAfter, balanceBefore + claimableBalance, "_helperUpdateMemberUnits: Pending balance not claimed" + ); + } + + // Assert that the flow rate for a member is updated accordingly + { + uint128 totalUnits = pool_.getTotalUnits(); + uint128 flowRatePerUnit = totalUnits == 0 ? 0 : uint128(uint96(pool_.getTotalFlowRate())) / totalUnits; + assertEq( + flowRatePerUnit * newUnits_, + uint128(uint96(pool_.getMemberFlowRate(member_))), + "_helperUpdateMemberUnits: Member flow rate incorrect" + ); + } + + // Update Expected Member Data + if (newUnits_ > 0) { + // @note You are only considered a member if you are given units + _poolMembers[address(pool_)].add(member_); + } + + // Assert Pool Total, Connected and Disconnect Units are correct + { + int256 unitsDelta = uint256(newUnits_).toInt256() - oldUnits; + assertEq( + uint256(uint256(poolUnitDataBefore.totalUnits).toInt256() + unitsDelta), + poolUnitDataAfter.totalUnits, + "_helperUpdateMemberUnits: Pool total units incorrect" + ); + assertEq( + uint256(uint256(poolUnitDataBefore.connectedUnits).toInt256() + (isConnected ? unitsDelta : int128(0))), + poolUnitDataAfter.connectedUnits, + "_helperUpdateMemberUnits: Pool connected units incorrect" + ); + assertEq( + uint256( + uint256(poolUnitDataBefore.disconnectedUnits).toInt256() + (isConnected ? int128(0) : unitsDelta) + ), + poolUnitDataAfter.disconnectedUnits, + "_helperUpdateMemberUnits: Pool disconnected units incorrect" + ); + } + + // Assert Pool Member NFT is minted/burned + _assertPoolMemberNFT(poolSuperToken, pool_, member_, newUnits_); + + // Assert RTB for all users + // _assertRealTimeBalances(ISuperToken(address(poolSuperToken))); + } + + function _helperConnectPool(address caller_, ISuperToken superToken_, ISuperfluidPool pool_, bool useForwarder_) + internal + { + (bool isConnectedBefore, int256 oldUnits, int96 oldFlowRate) = _helperGetMemberPoolState(pool_, caller_); + + PoolUnitData memory poolUnitDataBefore = _helperGetPoolUnitsData(pool_); + PoolFlowRateData memory poolFlowRateDataBefore = _helperGetPoolFlowRatesData(pool_); + + vm.startPrank(caller_); + if (useForwarder_) { + sf.gdaV1Forwarder.connectPool(pool_, ""); + } else { + sf.host.callAgreement( + sf.gda, + abi.encodeWithSelector(IGeneralDistributionAgreementV1.connectPool.selector, pool_, ""), + new bytes(0) + ); + } + vm.stopPrank(); + + PoolUnitData memory poolUnitDataAfter = _helperGetPoolUnitsData(pool_); + PoolFlowRateData memory poolFlowRateDataAfter = _helperGetPoolFlowRatesData(pool_); + + { + _helperTakeBalanceSnapshot(superToken_, caller_); + } + + bool isMemberConnected = useForwarder_ + ? sf.gdaV1Forwarder.isMemberConnected(pool_, caller_) + : sf.gda.isMemberConnected(pool_, caller_); + assertEq(isMemberConnected, true, "GDAv1.t: Member not connected"); + + // Assert connected units delta for the pool + { + assertEq( + isConnectedBefore ? 0 : uint256(oldUnits), + poolUnitDataAfter.connectedUnits - poolUnitDataBefore.connectedUnits, + "_helperConnectPool: Pool connected units incorrect" + ); + } + + // Assert connected and disconnected flow rate for the pool + { + assertEq( + poolFlowRateDataBefore.totalConnectedFlowRate + (isConnectedBefore ? int96(0) : oldFlowRate), + poolFlowRateDataAfter.totalConnectedFlowRate, + "_helperConnectPool: Pool connected flow rate incorrect" + ); + assertEq( + poolFlowRateDataBefore.totalDisconnectedFlowRate - (isConnectedBefore ? int96(0) : oldFlowRate), + poolFlowRateDataAfter.totalDisconnectedFlowRate, + "_helperConnectPool: Pool disconnected flow rate incorrect" + ); + } + // Assert RTB for all users + // _assertRealTimeBalances(superToken_); + _assertGlobalInvariants(); + } + + function _helperConnectPool(address caller_, ISuperToken superToken_, ISuperfluidPool pool_) internal { + _helperConnectPool(caller_, superToken_, pool_, false); + } + + function _helperDisconnectPool(address caller_, ISuperToken superToken_, ISuperfluidPool pool_, bool useForwarder_) + internal + { + (bool isConnectedBefore, int256 oldUnits, int96 oldFlowRate) = _helperGetMemberPoolState(pool_, caller_); + + PoolUnitData memory poolUnitDataBefore = _helperGetPoolUnitsData(pool_); + PoolFlowRateData memory poolFlowRateDataBefore = _helperGetPoolFlowRatesData(pool_); + + vm.startPrank(caller_); + if (useForwarder_) { + sf.gdaV1Forwarder.disconnectPool(pool_, ""); + } else { + sf.host.callAgreement(sf.gda, abi.encodeCall(sf.gda.disconnectPool, (pool_, new bytes(0))), new bytes(0)); + } + vm.stopPrank(); + + PoolUnitData memory poolUnitDataAfter = _helperGetPoolUnitsData(pool_); + PoolFlowRateData memory poolFlowRateDataAfter = _helperGetPoolFlowRatesData(pool_); + + { + _helperTakeBalanceSnapshot(superToken_, caller_); + } + + assertEq( + sf.gda.isMemberConnected(pool_, caller_), + false, + "GDAv1.t D/C: Member not disconnected" + ); + + // Assert disconnected units delta for the pool + { + assertEq( + isConnectedBefore ? uint256(oldUnits) : 0, + poolUnitDataAfter.disconnectedUnits - poolUnitDataBefore.disconnectedUnits, + "_helperDisconnectPool: Pool disconnected units incorrect" + ); + } + { + assertEq( + poolFlowRateDataBefore.totalConnectedFlowRate - (isConnectedBefore ? oldFlowRate : int96(0)), + poolFlowRateDataAfter.totalConnectedFlowRate, + "_helperDisconnectPool: Pool connected flow rate incorrect" + ); + assertEq( + poolFlowRateDataBefore.totalDisconnectedFlowRate + (isConnectedBefore ? oldFlowRate : int96(0)), + poolFlowRateDataAfter.totalDisconnectedFlowRate, + "_helperDisconnectPool: Pool disconnected flow rate incorrect" + ); + } + + // Assert RTB for all users + // _assertRealTimeBalances(superToken_); + _assertGlobalInvariants(); + } + + function _helperDisconnectPool(address caller_, ISuperToken superToken_, ISuperfluidPool pool_) internal { + _helperDisconnectPool(caller_, superToken_, pool_, false); + } + + function _helperDistributeViaGDA( + ISuperToken superToken_, + address caller_, + address from_, + ISuperfluidPool pool_, + uint256 requestedAmount, + bool useForwarder + ) internal { + (int256 fromRTBBefore,,,) = superToken.realtimeBalanceOfNow(from_); + + uint256 actualAmountDistributed = useForwarder + ? sf.gdaV1Forwarder.estimateDistributionActualAmount(superToken, from_, pool_, requestedAmount) + : sf.gda.estimateDistributionActualAmount(superToken, from_, pool_, requestedAmount); + + address[] memory members = _poolMembers[address(pool_)].values(); + uint256[] memory memberBalancesBefore = new uint256[](members.length); + uint256[] memory memberClaimableBefore = new uint256[](members.length); + + for (uint256 i = 0; i < members.length; ++i) { + (int256 memberRTB,,,) = superToken.realtimeBalanceOfNow(members[i]); + memberBalancesBefore[i] = uint256(memberRTB); + (int256 claimable,) = pool_.getClaimableNow(members[i]); + memberClaimableBefore[i] = uint256(claimable); + } + + { + vm.startPrank(caller_); + if (useForwarder) { + sf.gdaV1Forwarder.distribute(superToken_, from_, pool_, requestedAmount, new bytes(0)); + } else { + superToken_.distributeToPool(from_, pool_, requestedAmount); + } + vm.stopPrank(); + } + + { + _helperTakeBalanceSnapshot(superToken_, from_); + } + + uint256 amountPerUnit = pool_.getTotalUnits() > 0 ? actualAmountDistributed / pool_.getTotalUnits() : 0; + + // Assert Distributor RTB + { + (int256 fromRTBAfter,,,) = superToken.realtimeBalanceOfNow(from_); + // If the distributor is a connected member themselves, they will receive the units + // they have just distributed + uint256 amountReceivedInitial = sf.gda.isMemberConnected(pool_, from_) + ? uint256(pool_.getUnits(from_)) * amountPerUnit + : 0; + assertEq( + fromRTBAfter, + fromRTBBefore - int256(actualAmountDistributed) + int256(amountReceivedInitial), + "GDAv1.t D: Distributor RTB incorrect" + ); + } + + if (members.length == 0) return; + + // Assert Members RTB + for (uint256 i; i < members.length; ++i) { + (int256 memberRTB,,,) = superToken.realtimeBalanceOfNow(members[i]); + bool memberConnected = sf.gda.isMemberConnected(pool_, members[i]); + + uint256 amountReceived = uint256(pool_.getUnits(members[i])) * amountPerUnit; + if (memberConnected) { + if (members[i] == from_) { + assertEq( + memberRTB, + int256(memberBalancesBefore[i]) - int256(actualAmountDistributed) + int256(amountReceived), + "GDAv1.t D: Distributor who is Member RTB incorrect" + ); + } else { + assertEq( + uint256(memberRTB), memberBalancesBefore[i] + amountReceived, "GDAv1.t D: Member RTB incorrect" + ); + } + } else { + (int256 claimable,) = pool_.getClaimableNow(members[i]); + assertEq( + uint256(claimable), + amountReceived + uint256(memberClaimableBefore[i]), + "GDAv1.t D: Member claimable incorrect" + ); + } + } + + // Assert RTB for all users + // _assertRealTimeBalances(superToken_); + _assertGlobalInvariants(); + } + + function _helperDistributeViaGDA( + ISuperToken superToken_, + address caller_, + address from_, + ISuperfluidPool pool_, + uint256 requestedAmount + ) internal { + _helperDistributeViaGDA(superToken_, caller_, from_, pool_, requestedAmount, false); + } + + function _helperDistributeFlow( + ISuperToken superToken_, + address caller, + address from, + ISuperfluidPool pool_, + int96 requestedFlowRate, + bool useForwarder + ) internal { + (int96 actualFlowRate, int96 totalDistributionFlowRate) = useForwarder + ? sf.gdaV1Forwarder.estimateFlowDistributionActualFlowRate(superToken_, from, pool_, requestedFlowRate) + : sf.gda.estimateFlowDistributionActualFlowRate(superToken_, from, pool_, requestedFlowRate); + + address[] memory members = _poolMembers[address(pool_)].values(); + int96[] memory memberFlowRatesBefore = new int96[](members.length); + + for (uint256 i = 0; i < members.length; ++i) { + int96 memberFlowRate = pool_.getMemberFlowRate(members[i]); + memberFlowRatesBefore[i] = memberFlowRate; + } + + vm.startPrank(caller); + if (useForwarder) { + sf.gdaV1Forwarder.distributeFlow(superToken_, from, pool_, requestedFlowRate, new bytes(0)); + } else { + superToken_.distributeFlow(from, pool_, requestedFlowRate); + } + vm.stopPrank(); + + { + _helperTakeBalanceSnapshot(superToken_, from); + } + + int96 poolTotalFlowRateAfter = pool_.getTotalFlowRate(); + { + // Assert distributor flow rate + int96 fromToPoolFlowRateAfter = useForwarder + ? sf.gdaV1Forwarder.getFlowDistributionFlowRate(superToken_, from, pool_) + : sf.gda.getFlowRate(superToken_, from, pool_); + assertEq( + fromToPoolFlowRateAfter, + actualFlowRate, + "_helperDistributeFlow: from flow rate should be actual flow rate" + ); + + // Assert pool total flow rate + assertEq( + poolTotalFlowRateAfter, + totalDistributionFlowRate, + "_helperDistributeFlow: pool total flow rate != total distribution flow rate" + ); + } + + // Assert Outflow NFT is minted to distributor + // Assert Inflow NFT is minted to pool + _assertFlowNftOnDistributeFlow(superToken_, pool_, from, requestedFlowRate); + + { + if (members.length == 0) return; + uint128 poolTotalUnitsAfter = pool_.getTotalUnits(); + int96 flowRatePerUnit = poolTotalUnitsAfter == 0 + ? int96(0) + : poolTotalFlowRateAfter / uint256(poolTotalUnitsAfter).toInt256().toInt96(); + + for (uint256 i; i < members.length; ++i) { + int96 memberFlowRate = pool_.getMemberFlowRate(members[i]); + uint128 memberUnits = pool_.getUnits(members[i]); + int96 expectedMemberFlowRate = flowRatePerUnit * uint256(memberUnits).toInt256().toInt96(); + assertEq( + expectedMemberFlowRate, + memberFlowRate, + "_helperDistributeFlow: member flow rate != expected member flow rate" + ); + } + } + // Assert RTB for all users + // _assertRealTimeBalances(superToken_); + _assertGlobalInvariants(); + } + + function _helperDistributeFlow( + ISuperToken superToken_, + address caller, + address from, + ISuperfluidPool pool_, + int96 requestedFlowRate + ) internal { + _helperDistributeFlow(superToken_, caller, from, pool_, requestedFlowRate, false); + } + + // Write Helpers - SuperfluidPool ERC20 Functionality + + function _helperSuperfluidPoolApprove(ISuperfluidPool _pool, address owner, address spender, uint256 amount) + internal + { + vm.startPrank(owner); + _pool.approve(spender, amount); + vm.stopPrank(); + + _assertPoolAllowance(_pool, owner, spender, amount); + } + + function _helperSuperfluidPoolIncreaseAllowance( + ISuperfluidPool _pool, + address owner, + address spender, + uint256 addedValue + ) internal { + uint256 allowanceBefore = _pool.allowance(owner, spender); + + vm.startPrank(owner); + _pool.increaseAllowance(spender, addedValue); + vm.stopPrank(); + + _assertPoolAllowance(_pool, owner, spender, allowanceBefore + addedValue); + } + + function _helperSuperfluidPoolDecreaseAllowance( + ISuperfluidPool _pool, + address owner, + address spender, + uint256 subtractedValue + ) internal { + uint256 allowanceBefore = _pool.allowance(owner, spender); + + vm.startPrank(owner); + _pool.decreaseAllowance(spender, subtractedValue); + vm.stopPrank(); + + _assertPoolAllowance(_pool, owner, spender, allowanceBefore - subtractedValue); + } + + function _helperSuperfluidPoolUnitsTransfer(ISuperfluidPool _pool, address from, address to, uint256 amount) + internal + { + uint256 fromBalanceOfBefore = _pool.balanceOf(from); + uint256 toBalanceOfBefore = _pool.balanceOf(to); + + vm.startPrank(from); + _pool.transfer(to, amount); + vm.stopPrank(); + + uint256 fromBalanceOfAfter = _pool.balanceOf(from); + uint256 toBalanceOfAfter = _pool.balanceOf(to); + assertEq( + fromBalanceOfBefore - amount, + fromBalanceOfAfter, + "_helperSuperfluidPoolUnitsTransfer: from balance mismatch" + ); + assertEq( + toBalanceOfBefore + amount, toBalanceOfAfter, "_helperSuperfluidPoolUnitsTransfer: to balance mismatch" + ); + } + + function _helperSuperfluidPoolUnitsTransferFrom( + ISuperfluidPool _pool, + address caller, + address from, + address to, + uint256 amount + ) internal { + uint256 fromBalanceOfBefore = _pool.balanceOf(from); + uint256 toBalanceOfBefore = _pool.balanceOf(to); + uint256 allowanceBefore = _pool.allowance(from, caller); + + vm.startPrank(caller); + _pool.transferFrom(from, to, amount); + vm.stopPrank(); + + uint256 fromBalanceOfAfter = _pool.balanceOf(from); + uint256 toBalanceOfAfter = _pool.balanceOf(to); + uint256 allowanceAfter = _pool.allowance(from, caller); + assertEq( + fromBalanceOfBefore - amount, + fromBalanceOfAfter, + "_helperSuperfluidPoolUnitsTransferFrom: from balance mismatch" + ); + assertEq( + toBalanceOfBefore + amount, toBalanceOfAfter, "_helperSuperfluidPoolUnitsTransferFrom: to balance mismatch" + ); + assertEq(allowanceBefore - amount, allowanceAfter, "_helperSuperfluidPoolUnitsTransferFrom: allowance mismatch"); + } + + function _helperGetMemberPoolState(ISuperfluidPool pool_, address member_) + internal + view + returns (bool isConnected, int256 units, int96 flowRate) + { + units = uint256(pool_.getUnits(member_)).toInt256(); + isConnected = sf.gda.isMemberConnected(pool_, member_); + flowRate = pool_.getMemberFlowRate(member_); + } + + function _helperGetPoolUnitsData(ISuperfluidPool pool_) internal view returns (PoolUnitData memory poolUnitData) { + poolUnitData = PoolUnitData({ + totalUnits: pool_.getTotalUnits(), + connectedUnits: pool_.getTotalConnectedUnits(), + disconnectedUnits: pool_.getTotalDisconnectedUnits() + }); + } + + function _helperGetPoolFlowRatesData(ISuperfluidPool pool_) + internal + view + returns (PoolFlowRateData memory poolFlowRateData) + { + poolFlowRateData = PoolFlowRateData({ + totalFlowRate: pool_.getTotalFlowRate(), + totalConnectedFlowRate: pool_.getTotalConnectedFlowRate(), + totalDisconnectedFlowRate: pool_.getTotalDisconnectedFlowRate() + }); + } + /*////////////////////////////////////////////////////////////////////////// Assertion Helpers //////////////////////////////////////////////////////////////////////////*/ @@ -1482,7 +2174,6 @@ contract FoundrySuperfluidTester is Test { ) internal { (uint256 lastUpdated, int96 netFlowRate, uint256 deposit, uint256 owedDeposit) = sf.cfa.getAccountFlowInfo(superToken, account); - int96 expectedNetFlowRate = flowInfoBefore.flowRate + (isSender ? -flowRateDelta : flowRateDelta); int256 depositDelta = superToken.getBufferAmountByFlowRate(flowRateDelta < 0 ? -flowRateDelta : flowRateDelta).toInt256(); @@ -1497,6 +2188,8 @@ contract FoundrySuperfluidTester is Test { assertEq(owedDeposit, 0, "AccountFlowInfo: owed deposit"); } + // InstantDistributionAgreement Assertions + /// @dev Asserts that the index data has been updated as expected /// @param superToken_ The SuperToken to check /// @param publisher The publisher of the index @@ -1552,8 +2245,13 @@ contract FoundrySuperfluidTester is Test { RealtimeBalance memory balanceSnapshot = _balanceSnapshots[superToken_][account]; (int256 avb, uint256 deposit, uint256 owedDeposit, uint256 currentTime) = superToken_.realtimeBalanceOfNow(account); + int96 cfaNetFlowRate = superToken_.getCFANetFlowRate(account); - int96 netFlowRate = superToken_.getNetFlowRate(account); + // GDA Net Flow Rate is 0 for pools because this is not accounted for in the pools' RTB + // however it is the disconnected flow rate for that pool + int96 gdaNetFlowRate = + sf.gda.isPool(superToken_, account) ? int96(0) : superToken_.getGDANetFlowRate(account); + int96 netFlowRate = cfaNetFlowRate + gdaNetFlowRate; int256 amountFlowedSinceSnapshot = (currentTime - balanceSnapshot.timestamp).toInt256() * netFlowRate; int256 expectedAvb = balanceSnapshot.availableBalance + amountFlowedSinceSnapshot; @@ -1564,4 +2262,64 @@ contract FoundrySuperfluidTester is Test { _helperTakeBalanceSnapshot(superToken_, account); } } + + // GeneralDistributionAgreement Assertions + + function _assertPoolAllowance(ISuperfluidPool _pool, address owner, address spender, uint256 expectedAllowance) + internal + { + assertEq(_pool.allowance(owner, spender), expectedAllowance, "_assertPoolAllowance: allowance mismatch"); + } + + function _assertPoolMemberNFT( + ISuperfluidToken _superToken, + ISuperfluidPool _pool, + address _member, + uint128 _newUnits + ) internal { + IPoolMemberNFT poolMemberNFT = SuperToken(address(_superToken)).POOL_MEMBER_NFT(); + uint256 tokenId = poolMemberNFT.getTokenId(address(_pool), address(_member)); + if (_newUnits > 0) { + // Assert Pool Member NFT owner + assertEq(poolMemberNFT.ownerOf(tokenId), _member, "_assertPoolMemberNFT: member doesn't own NFT"); + + // Assert Pool Member NFT data + IPoolMemberNFT.PoolMemberNFTData memory poolMemberData = poolMemberNFT.poolMemberDataByTokenId(tokenId); + assertEq(poolMemberData.pool, address(_pool), "_assertPoolMemberNFT: Pool Member NFT pool mismatch"); + assertEq(poolMemberData.member, _member, "_assertPoolMemberNFT: Pool Member NFT member mismatch"); + assertEq(poolMemberData.units, _newUnits, "_assertPoolMemberNFT: Pool Member NFT units mismatch"); + } else { + vm.expectRevert(IPoolNFTBase.POOL_NFT_INVALID_TOKEN_ID.selector); + poolMemberNFT.ownerOf(tokenId); + } + } + + function _assertFlowNftOnDistributeFlow( + ISuperfluidToken _superToken, + ISuperfluidPool _pool, + address _distributor, + int96 _newFlowRate + ) internal { + IConstantOutflowNFT constantOutflowNFT = SuperToken(address(_superToken)).CONSTANT_OUTFLOW_NFT(); + IConstantInflowNFT constantInflowNFT = SuperToken(address(_superToken)).CONSTANT_INFLOW_NFT(); + uint256 tokenId = constantOutflowNFT.getTokenId(address(_superToken), address(_distributor), address(_pool)); + if (_newFlowRate > 0) { + assertEq( + constantOutflowNFT.ownerOf(tokenId), + _distributor, + "_assertFlowNftOnDistributeFlow: distributor doesn't own outflow NFT" + ); + assertEq( + constantInflowNFT.ownerOf(tokenId), + address(_pool), + "_assertFlowNftOnDistributeFlow: distributor doesn't own inflow NFT" + ); + } else { + vm.expectRevert(IFlowNFTBase.CFA_NFT_INVALID_TOKEN_ID.selector); + constantOutflowNFT.ownerOf(tokenId); + + vm.expectRevert(IFlowNFTBase.CFA_NFT_INVALID_TOKEN_ID.selector); + constantInflowNFT.ownerOf(tokenId); + } + } } diff --git a/packages/ethereum-contracts/test/foundry/SuperfluidFrameworkDeployer.t.sol b/packages/ethereum-contracts/test/foundry/SuperfluidFrameworkDeployer.t.sol index 4df2f141d7..08123e9936 100644 --- a/packages/ethereum-contracts/test/foundry/SuperfluidFrameworkDeployer.t.sol +++ b/packages/ethereum-contracts/test/foundry/SuperfluidFrameworkDeployer.t.sol @@ -13,6 +13,7 @@ contract SuperfluidFrameworkDeployerTest is FoundrySuperfluidTester { assertTrue(address(sf.host) != address(0), "SFDeployer: host not deployed"); assertTrue(address(sf.cfa) != address(0), "SFDeployer: cfa not deployed"); assertTrue(address(sf.ida) != address(0), "SFDeployer: ida not deployed"); + assertTrue(address(sf.gda) != address(0), "SFDeployer: gda not deployed"); assertTrue(address(sf.superTokenFactory) != address(0), "SFDeployer: superTokenFactory not deployed"); assertTrue(address(sf.superTokenLogic) != address(0), "SFDeployer: superTokenLogic not deployed"); assertTrue(address(sf.constantOutflowNFT) != address(0), "SFDeployer: constantOutflowNFT not deployed"); @@ -20,6 +21,9 @@ contract SuperfluidFrameworkDeployerTest is FoundrySuperfluidTester { assertTrue(address(sf.resolver) != address(0), "SFDeployer: resolver not deployed"); assertTrue(address(sf.superfluidLoader) != address(0), "SFDeployer: superfluidLoader not deployed"); assertTrue(address(sf.cfaV1Forwarder) != address(0), "SFDeployer: cfaV1Forwarder not deployed"); + assertTrue(address(sf.idaV1Forwarder) != address(0), "SFDeployer: idaV1Forwarder not deployed"); + assertTrue(address(sf.gdaV1Forwarder) != address(0), "SFDeployer: gdaV1Forwarder not deployed"); + assertTrue(address(sf.batchLiquidator) != address(0), "SFDeployer: batchLiquidator not deployed"); } function testResolverGetsGovernance() public { @@ -63,7 +67,7 @@ contract SuperfluidFrameworkDeployerTest is FoundrySuperfluidTester { uint256 _mintLimit ) public { (TestToken underlyingToken, SuperToken _superToken) = - sfDeployer.deployWrapperSuperToken(_name, _symbol, _decimals, _mintLimit); + sfDeployer.deployWrapperSuperToken(_name, _symbol, _decimals, _mintLimit, address(0)); // assert underlying erc20 name/symbol properly set assertEq(underlyingToken.name(), _name, "SFDeployer: Underlying token name not properly set"); diff --git a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol new file mode 100644 index 0000000000..9d9b80e727 --- /dev/null +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -0,0 +1,933 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { IBeacon } from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; +import "@superfluid-finance/solidity-semantic-money/src/SemanticMoney.sol"; +import "../../FoundrySuperfluidTester.sol"; +import { + GeneralDistributionAgreementV1, + IGeneralDistributionAgreementV1 +} from "../../../../contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol"; +import { SuperTokenV1Library } from "../../../../contracts/apps/SuperTokenV1Library.sol"; +import { ISuperToken, SuperToken } from "../../../../contracts/superfluid/SuperToken.sol"; +import { ISuperfluidToken } from "../../../../contracts/interfaces/superfluid/ISuperfluidToken.sol"; +import { ISuperfluidPool, SuperfluidPool } from "../../../../contracts/agreements/gdav1/SuperfluidPool.sol"; +import { SuperfluidPoolStorageLayoutMock } from "../../../../contracts/mocks/SuperfluidPoolUpgradabilityMock.sol"; +import { IPoolNFTBase } from "../../../../contracts/interfaces/agreements/gdav1/IPoolNFTBase.sol"; +import { IPoolAdminNFT } from "../../../../contracts/interfaces/agreements/gdav1/IPoolAdminNFT.sol"; +import { IPoolMemberNFT } from "../../../../contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol"; +import { IFlowNFTBase } from "../../../../contracts/interfaces/superfluid/IFlowNFTBase.sol"; +import { IConstantOutflowNFT } from "../../../../contracts/interfaces/superfluid/IConstantOutflowNFT.sol"; +import { IConstantInflowNFT } from "../../../../contracts/interfaces/superfluid/IConstantInflowNFT.sol"; + +/// @title GeneralDistributionAgreementV1 Integration Tests +/// @author Superfluid +/// @notice This is a contract that runs integrations tests for the GDAv1 +/// It tests interactions between contracts and more complicated interactions +/// with a range of values when applicable and it aims to ensure that the +/// these interactions work as expected. +contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTester { + using SuperTokenV1Library for ISuperToken; + using EnumerableSet for EnumerableSet.AddressSet; + using SafeCast for uint256; + using SafeCast for int256; + + struct UpdateMemberData { + address member; + uint64 newUnits; + } + + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + /// @dev The freePool uses `poolConfig` where both transfer and distributeFromAnyAddress is true + SuperfluidPool public freePool; + uint256 public liquidationPeriod; + + constructor() FoundrySuperfluidTester(7) { } + + function setUp() public override { + super.setUp(); + vm.startPrank(alice); + freePool = SuperfluidPool(address(superToken.createPool(alice, poolConfig))); + _addAccount(address(freePool)); + vm.stopPrank(); + (liquidationPeriod,) = sf.governance.getPPPConfig(sf.host, superToken); + } + + function _getMembers(uint8 length) internal view returns (address[] memory) { + if (length > TEST_ACCOUNTS.length - 2) revert("Too many members"); + address[] memory members = new address[](length); + for (uint8 i = 0; i < length; ++i) { + // do not use Admin and Alice + members[i] = TEST_ACCOUNTS[i + 2]; + } + return members; + } + + /*////////////////////////////////////////////////////////////////////////// + GDA Integration Tests + //////////////////////////////////////////////////////////////////////////*/ + + function testInitializeGDA(IBeacon beacon) public { + GeneralDistributionAgreementV1 gdaV1 = new GeneralDistributionAgreementV1(sf.host); + assertEq(address(gdaV1.superfluidPoolBeacon()), address(0), "GDAv1.t: Beacon address not address(0)"); + gdaV1.initialize(beacon); + + assertEq(address(gdaV1.superfluidPoolBeacon()), address(beacon), "GDAv1.t: Beacon address not equal"); + } + + function testRevertReinitializeGDA(IBeacon beacon) public { + vm.expectRevert("Initializable: contract is already initialized"); + sf.gda.initialize(beacon); + } + + function testRevertAppendIndexUpdateByPoolByNonPool(BasicParticle memory p, Time t) public { + vm.expectRevert(IGeneralDistributionAgreementV1.GDA_ONLY_SUPER_TOKEN_POOL.selector); + sf.gda.appendIndexUpdateByPool(superToken, p, t); + } + + function testRevertPoolSettleClaimByNonPool(address claimRecipient, int256 amount) public { + vm.expectRevert(IGeneralDistributionAgreementV1.GDA_ONLY_SUPER_TOKEN_POOL.selector); + sf.gda.poolSettleClaim(superToken, claimRecipient, amount); + } + + function testProxiableUUIDIsExpectedValue(PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + assertEq( + SuperfluidPool(address(pool)).proxiableUUID(), + keccak256("org.superfluid-finance.contracts.SuperfluidPool.implementation") + ); + } + + function testPositiveBalanceIsPatricianPeriodNow(address account) public { + (bool isPatricianPeriod,) = sf.gda.isPatricianPeriodNow(superToken, account); + assertEq(isPatricianPeriod, true); + } + + function testNegativeBalanceIsPatricianPeriodNowIsTrue(PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + uint256 balance = superToken.balanceOf(alice); + int96 flowRate = balance.toInt256().toInt96() / type(int32).max; + int96 requestedDistributionFlowRate = int96(flowRate); + + _helperConnectPool(bob, superToken, pool); + _helperUpdateMemberUnits(pool, alice, bob, 1); + + _helperDistributeFlow(superToken, alice, alice, pool, requestedDistributionFlowRate); + + _helperWarpToCritical(superToken, alice, 1); + + (bool isPatricianPeriod,) = sf.gda.isPatricianPeriodNow(superToken, alice); + assertEq(isPatricianPeriod, true); + } + + function testNegativeBalanceIsPatricianPeriodNowIsFalse(PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + uint256 balance = superToken.balanceOf(alice); + int96 flowRate = balance.toInt256().toInt96() / type(int32).max; + int96 requestedDistributionFlowRate = int96(flowRate); + + _helperConnectPool(bob, superToken, pool); + _helperUpdateMemberUnits(pool, alice, bob, 1); + + (int96 actualDistributionFlowRate,) = + sf.gda.estimateFlowDistributionActualFlowRate(superToken, alice, pool, requestedDistributionFlowRate); + + _helperDistributeFlow(superToken, alice, alice, pool, requestedDistributionFlowRate); + + if (actualDistributionFlowRate > 0) { + _helperWarpToInsolvency(superToken, alice, liquidationPeriod, 1); + } + + (bool isPatricianPeriod,) = sf.gda.isPatricianPeriodNow(superToken, alice); + assertEq(isPatricianPeriod, false); + } + + function testNegativeBalanceIsPatricianPeriodNowIsFalseWithZeroDeposit(PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + uint256 aliceBalance = superToken.balanceOf(alice); + int96 flowRate = aliceBalance.toInt256().toInt96() / type(int32).max; + int96 requestedDistributionFlowRate = int96(flowRate); + + vm.startPrank(sf.governance.owner()); + sf.governance.setRewardAddress(sf.host, ISuperfluidToken(address(0)), alice); + vm.stopPrank(); + + _helperConnectPool(bob, superToken, pool); + _helperUpdateMemberUnits(pool, alice, bob, 1); + + (int256 aliceRTB, uint256 deposit,,) = superToken.realtimeBalanceOfNow(alice); + + _helperDistributeFlow(superToken, alice, alice, pool, requestedDistributionFlowRate); + int96 fr = sf.gda.getFlowRate(superToken, alice, pool); + + vm.warp(block.timestamp + (INIT_SUPER_TOKEN_BALANCE / uint256(uint96(fr))) + 1); + + (aliceRTB, deposit,,) = superToken.realtimeBalanceOfNow(alice); + + _helperDistributeFlow(superToken, bob, alice, pool, 0); + + (bool isPatricianPeriod,) = sf.gda.isPatricianPeriodNow(superToken, alice); + assertEq(isPatricianPeriod, false, "false patrician period"); + } + + function testCreatePool(bool useForwarder, PoolConfig memory config) public { + _helperCreatePool(superToken, alice, alice, useForwarder, config); + } + + function testRevertConnectPoolByNonHost(address notHost, PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + vm.assume(notHost != address(sf.host)); + vm.startPrank(notHost); + vm.expectRevert("unauthorized host"); + sf.gda.connectPool(pool, "0x"); + vm.stopPrank(); + } + + function testRevertNonHostDisconnectPool(address notHost, PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + vm.assume(notHost != address(sf.host)); + vm.startPrank(notHost); + vm.expectRevert("unauthorized host"); + sf.gda.disconnectPool(pool, "0x"); + vm.stopPrank(); + } + + function testConnectPool(address caller, bool useForwarder, PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, caller, alice, useForwarder, config); + _helperConnectPool(caller, superToken, pool, useForwarder); + } + + function testDisconnectPool(address caller, bool useForwarder, PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, caller, alice, useForwarder, config); + _helperConnectPool(caller, superToken, pool, useForwarder); + _helperDisconnectPool(caller, superToken, pool, useForwarder); + } + + function testRevertDistributeFlowToNonPool(int96 requestedFlowRate) public { + vm.assume(requestedFlowRate >= 0); + vm.assume(requestedFlowRate < int96(type(int64).max)); + vm.startPrank(alice); + vm.expectRevert(IGeneralDistributionAgreementV1.GDA_ONLY_SUPER_TOKEN_POOL.selector); + superToken.distributeFlow(alice, ISuperfluidPool(bob), requestedFlowRate); + vm.stopPrank(); + } + + function testRevertDistributeFromAnyAddressWhenNotAllowed(bool useForwarder) public { + PoolConfig memory config = PoolConfig({ transferabilityForUnitsOwner: true, distributionFromAnyAddress: false }); + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, useForwarder, config); + + vm.expectRevert(IGeneralDistributionAgreementV1.GDA_DISTRIBUTE_FROM_ANY_ADDRESS_NOT_ALLOWED.selector); + vm.startPrank(bob); + superToken.distributeToPool(bob, pool, 1); + vm.stopPrank(); + } + + function testRevertDistributeFlowFromAnyAddressWhenNotAllowed(bool useForwarder) public { + PoolConfig memory config = PoolConfig({ transferabilityForUnitsOwner: true, distributionFromAnyAddress: false }); + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, useForwarder, config); + + vm.expectRevert(IGeneralDistributionAgreementV1.GDA_DISTRIBUTE_FROM_ANY_ADDRESS_NOT_ALLOWED.selector); + vm.startPrank(bob); + superToken.distributeFlow(bob, pool, 1); + vm.stopPrank(); + } + + function testRevertIfNotAdminUpdatesMemberUnitsViaGDA(bool useForwarder, PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, useForwarder, config); + vm.startPrank(bob); + vm.expectRevert(IGeneralDistributionAgreementV1.GDA_NOT_POOL_ADMIN.selector); + superToken.updateMemberUnits(pool, bob, 69); + vm.stopPrank(); + } + + function testRevertIfNotAdminOrGDAUpdatesMemberUnitsViaPool(address caller) public { + vm.assume(caller != alice); + vm.startPrank(caller); + vm.expectRevert(ISuperfluidPool.SUPERFLUID_POOL_NOT_POOL_ADMIN_OR_GDA.selector); + freePool.updateMemberUnits(caller, 69); + vm.stopPrank(); + } + + function testRevertDistributeFlowWithNegativeFlowRate(int96 requestedFlowRate, PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + vm.assume(requestedFlowRate < 0); + + vm.startPrank(alice); + vm.expectRevert(IGeneralDistributionAgreementV1.GDA_NO_NEGATIVE_FLOW_RATE.selector); + superToken.distributeFlow(alice, pool, requestedFlowRate); + vm.stopPrank(); + } + + function testRevertDistributeToNonPool(uint256 requestedAmount) public { + vm.assume(requestedAmount < uint256(type(uint128).max)); + + vm.startPrank(alice); + vm.expectRevert(IGeneralDistributionAgreementV1.GDA_ONLY_SUPER_TOKEN_POOL.selector); + superToken.distributeToPool(alice, ISuperfluidPool(bob), requestedAmount); + vm.stopPrank(); + } + + function testRevertDistributeForOthers(address signer, uint256 requestedAmount) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, poolConfig); + vm.assume(requestedAmount < uint256(type(uint128).max)); + vm.assume(signer != alice); + + vm.startPrank(signer); + vm.expectRevert(IGeneralDistributionAgreementV1.GDA_DISTRIBUTE_FOR_OTHERS_NOT_ALLOWED.selector); + sf.host.callAgreement( + sf.gda, + abi.encodeCall(sf.gda.distribute, (superToken, alice, pool, requestedAmount, new bytes(0))), + new bytes(0) + ); + vm.stopPrank(); + } + + function testRevertDistributeFlowForOthers(address signer, int32 requestedFlowRate) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, poolConfig); + vm.assume(requestedFlowRate > 0); + vm.assume(signer != alice); + + vm.startPrank(signer); + vm.expectRevert(IGeneralDistributionAgreementV1.GDA_DISTRIBUTE_FOR_OTHERS_NOT_ALLOWED.selector); + sf.host.callAgreement( + sf.gda, + abi.encodeCall(sf.gda.distributeFlow, (superToken, alice, pool, requestedFlowRate, new bytes(0))), + new bytes(0) + ); + vm.stopPrank(); + } + + function testRevertDistributeFlowInsufficientBalance(PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + uint256 balance = superToken.balanceOf(alice); + balance /= 4 hours; + int96 tooBigFlowRate = int96(int256(balance)) + 1; + + _helperConnectPool(bob, superToken, pool); + + _helperUpdateMemberUnits(pool, alice, bob, 1); + vm.startPrank(alice); + vm.expectRevert(IGeneralDistributionAgreementV1.GDA_INSUFFICIENT_BALANCE.selector); + sf.host.callAgreement( + sf.gda, + abi.encodeCall(sf.gda.distributeFlow, (superToken, alice, pool, tooBigFlowRate, new bytes(0))), + new bytes(0) + ); + vm.stopPrank(); + } + + function testRevertLiquidateNonCriticalDistributor(int32 flowRate, int96 units, PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + vm.assume(flowRate > 0); + _helperConnectPool(bob, superToken, pool); + + _helperUpdateMemberUnits(pool, alice, bob, uint96(units)); + + _helperDistributeFlow(superToken, alice, alice, pool, flowRate); + + vm.startPrank(bob); + vm.expectRevert(IGeneralDistributionAgreementV1.GDA_NON_CRITICAL_SENDER.selector); + superToken.distributeFlow(alice, pool, 0); + vm.stopPrank(); + } + + function testRevertDistributeInsufficientBalance(PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + uint256 balance = superToken.balanceOf(alice); + + _helperConnectPool(bob, superToken, pool); + + vm.startPrank(alice); + sf.gdaV1Forwarder.updateMemberUnits(pool, bob, 1, new bytes(0)); + vm.stopPrank(); + + vm.startPrank(alice); + vm.expectRevert(IGeneralDistributionAgreementV1.GDA_INSUFFICIENT_BALANCE.selector); + sf.host.callAgreement( + sf.gda, + abi.encodeCall(sf.gda.distribute, (superToken, alice, pool, balance + 1, new bytes(0))), + new bytes(0) + ); + vm.stopPrank(); + } + + function testRevertPoolOperatorConnectMember( + address notOperator, + address member, + bool doConnect, + uint32 time, + PoolConfig memory config + ) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + vm.assume(notOperator != address(sf.gda)); + vm.startPrank(notOperator); + vm.expectRevert(ISuperfluidPool.SUPERFLUID_POOL_NOT_GDA.selector); + SuperfluidPool(address(pool)).operatorConnectMember(member, doConnect, time); + vm.stopPrank(); + } + + function testRevertPoolUpdateMemberThatIsPool(uint128 units, PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + vm.assume(units < uint128(type(int128).max)); + + vm.expectRevert(ISuperfluidPool.SUPERFLUID_POOL_NO_POOL_MEMBERS.selector); + vm.startPrank(alice); + pool.updateMemberUnits(address(pool), units); + vm.stopPrank(); + } + + function testSuperfluidPoolStorageLayout() public { + SuperfluidPoolStorageLayoutMock mock = new SuperfluidPoolStorageLayoutMock(sf.gda); + mock.validateStorageLayout(); + } + + function testDistributeFlowUsesMinDeposit( + uint64 distributionFlowRate, + uint32 minDepositMultiplier, + address member, + FoundrySuperfluidTester._StackVars_UseBools memory useBools_, + PoolConfig memory config + ) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + vm.assume(distributionFlowRate < minDepositMultiplier); + vm.assume(distributionFlowRate > 0); + vm.assume(member != address(pool)); + vm.assume(member != address(0)); + + _addAccount(member); + + vm.startPrank(address(sf.governance.owner())); + uint256 minimumDeposit = 4 hours * uint256(minDepositMultiplier); + sf.governance.setSuperTokenMinimumDeposit(sf.host, superToken, minimumDeposit); + vm.stopPrank(); + + _helperConnectPool(member, superToken, pool); + _helperUpdateMemberUnits(pool, alice, member, 1, useBools_); + _helperDistributeFlow(superToken, alice, alice, pool, int96(int64(distributionFlowRate))); + (, uint256 buffer,,) = superToken.realtimeBalanceOfNow(alice); + assertEq(buffer, minimumDeposit, "GDAv1.t: Min buffer should be used"); + } + + function testDistributeFlowIgnoresMinDeposit( + int32 distributionFlowRate, + uint32 minDepositMultiplier, + address member, + FoundrySuperfluidTester._StackVars_UseBools memory useBools_, + PoolConfig memory config + ) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + vm.assume(uint32(distributionFlowRate) >= minDepositMultiplier); + vm.assume(distributionFlowRate > 0); + vm.assume(member != address(0)); + vm.assume(member != address(freePool)); + + _addAccount(member); + + vm.startPrank(address(sf.governance.owner())); + + uint256 minimumDeposit = 4 hours * uint256(minDepositMultiplier); + sf.governance.setSuperTokenMinimumDeposit(sf.host, superToken, minimumDeposit); + vm.stopPrank(); + + _helperConnectPool(member, superToken, pool); + _helperUpdateMemberUnits(pool, alice, member, 1, useBools_); + _helperDistributeFlow(superToken, alice, alice, pool, int96(distributionFlowRate)); + (, uint256 buffer,,) = superToken.realtimeBalanceOfNow(alice); + assertTrue(buffer >= minimumDeposit, "GDAv1.t: Buffer should be >= minDeposit"); + } + + function testDistributeFlowToConnectedMemberSendingToCFA( + int32 flowRate, + uint64 units, + FoundrySuperfluidTester._StackVars_UseBools memory useBools_, + PoolConfig memory config + ) public { + vm.assume(flowRate > 0); + + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + + int96 requestedDistributionFlowRate = int96(flowRate); + + uint128 memberUnits = uint128(units); + + _helperUpdateMemberUnits(pool, alice, bob, memberUnits, useBools_); + + _helperDistributeFlow(superToken, alice, alice, pool, requestedDistributionFlowRate); + + _helperConnectPool(bob, superToken, pool); + // bob sends a flow of 1 to alice + vm.startPrank(bob); + superToken.createFlow(alice, requestedDistributionFlowRate * 10); + vm.stopPrank(); + + int96 aliceGDANetFlowRate = sf.gda.getNetFlow(superToken, alice); + int96 bobGDANetFlowRate = sf.gda.getNetFlow(superToken, bob); + int96 aliceCFANetFlowRate = sf.cfa.getNetFlow(superToken, alice); + int96 bobCFANetFlowRate = sf.cfa.getNetFlow(superToken, bob); + assertEq( + aliceGDANetFlowRate + bobGDANetFlowRate + aliceCFANetFlowRate + bobCFANetFlowRate, + 0, + "alice and bob GDA net flow rates !=" + ); + } + + function testDistributeToEmptyPool(uint64 distributionAmount, bool useForwarder, PoolConfig memory config) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, useForwarder, config); + _helperDistributeViaGDA(superToken, alice, alice, pool, distributionAmount, useForwarder); + } + + function testDistributeFlowToEmptyPool(int32 flowRate, bool useForwarder, PoolConfig memory config) public { + vm.assume(flowRate >= 0); + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, useForwarder, config); + _helperDistributeFlow(superToken, alice, alice, pool, flowRate, useForwarder); + assertEq(sf.gda.getFlowRate(superToken, alice, pool), 0, "GDAv1.t: distributionFlowRate should be 0"); + } + + function testDistributeFlowCriticalLiquidation( + uint64 units, + _StackVars_UseBools memory useBools_, + PoolConfig memory config + ) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + + uint256 balance = superToken.balanceOf(alice); + int96 flowRate = balance.toInt256().toInt96() / type(int32).max; + int96 requestedDistributionFlowRate = int96(flowRate); + + uint128 memberUnits = uint128(units); + + _helperConnectPool(bob, superToken, pool); + _helperUpdateMemberUnits(pool, alice, bob, memberUnits, useBools_); + + (int96 actualDistributionFlowRate,) = + sf.gda.estimateFlowDistributionActualFlowRate(superToken, alice, pool, requestedDistributionFlowRate); + + _helperDistributeFlow(superToken, alice, alice, pool, requestedDistributionFlowRate); + + if (actualDistributionFlowRate > 0) { + _helperWarpToCritical(superToken, alice, 1); + _helperDistributeFlow(superToken, bob, alice, pool, 0); + } + } + + function testDistributeFlowInsolventLiquidation( + uint64 units, + _StackVars_UseBools memory useBools_, + PoolConfig memory config + ) public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + + uint256 balance = superToken.balanceOf(alice); + int96 flowRate = balance.toInt256().toInt96() / type(int32).max; + int96 requestedDistributionFlowRate = int96(flowRate); + + uint128 memberUnits = uint128(units); + + _helperConnectPool(bob, superToken, pool); + _helperUpdateMemberUnits(pool, alice, bob, memberUnits, useBools_); + _helperDistributeFlow(superToken, alice, alice, pool, requestedDistributionFlowRate); + + (int96 actualDistributionFlowRate,) = + sf.gda.estimateFlowDistributionActualFlowRate(superToken, alice, pool, requestedDistributionFlowRate); + + _helperDistributeFlow(superToken, alice, alice, pool, requestedDistributionFlowRate); + + if (actualDistributionFlowRate > 0) { + _helperWarpToInsolvency(superToken, alice, liquidationPeriod, 1); + _helperDistributeFlow(superToken, bob, alice, pool, 0); + } + } + + function testDistributeToDisconnectedMembers( + uint64[5] memory memberUnits, + uint256 distributionAmount, + _StackVars_UseBools memory useBools_, + PoolConfig memory config + ) public { + address distributor = alice; + uint256 distributorBalance = superToken.balanceOf(distributor); + + vm.assume(distributionAmount < distributorBalance); + + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + + address[] memory members = _getMembers(5); + + for (uint256 i = 0; i < members.length; ++i) { + if (sf.gda.isPool(superToken, members[i]) || members[i] == address(0)) continue; + + _helperUpdateMemberUnits(pool, alice, members[i], memberUnits[i], useBools_); + } + _helperDistributeViaGDA(superToken, alice, alice, pool, distributionAmount, useBools_.useForwarder); + } + + function testDistributeToConnectedMembers( + uint64[5] memory memberUnits, + uint256 distributionAmount, + _StackVars_UseBools memory useBools_, + PoolConfig memory config + ) public { + address distributor = alice; + uint256 distributorBalance = superToken.balanceOf(distributor); + + vm.assume(distributionAmount < distributorBalance); + + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + + address[] memory members = _getMembers(5); + + for (uint256 i = 0; i < members.length; ++i) { + if (sf.gda.isPool(superToken, members[i]) || members[i] == address(0)) continue; + + _helperConnectPool(members[i], superToken, pool, useBools_.useForwarder); + _helperUpdateMemberUnits(pool, alice, members[i], memberUnits[i], useBools_); + _addAccount(members[i]); + } + _helperDistributeViaGDA(superToken, alice, alice, pool, distributionAmount, useBools_.useForwarder); + } + + function testDistributeFlowToConnectedMembers( + uint64[5] memory memberUnits, + int32 flowRate, + _StackVars_UseBools memory useBools_, + PoolConfig memory config + ) public { + vm.assume(flowRate > 0); + + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + + address[] memory members = _getMembers(5); + + for (uint256 i = 0; i < members.length; ++i) { + if (sf.gda.isPool(superToken, members[i]) || members[i] == address(0)) continue; + + _helperConnectPool(members[i], superToken, pool, useBools_.useForwarder); + _helperUpdateMemberUnits(pool, alice, members[i], memberUnits[i], useBools_); + _addAccount(members[i]); + } + + _helperDistributeFlow(superToken, alice, alice, pool, 100, useBools_.useForwarder); + int96 poolAdjustmentFlowRate = useBools_.useForwarder + ? sf.gdaV1Forwarder.getPoolAdjustmentFlowRate(address(pool)) + : sf.gda.getPoolAdjustmentFlowRate(address(pool)); + assertEq(poolAdjustmentFlowRate, 0, "GDAv1.t: Pool adjustment rate is non-zero"); + } + + function testDistributeFlowToUnconnectedMembers( + uint64[5] memory memberUnits, + int32 flowRate, + uint16 warpTime, + _StackVars_UseBools memory useBools_, + PoolConfig memory config + ) public { + vm.assume(flowRate > 0); + + ISuperfluidPool pool; + { + pool = _helperCreatePool(superToken, alice, alice, false, config); + } + + address[] memory members = _getMembers(5); + + for (uint256 i = 0; i < members.length; ++i) { + if (sf.gda.isPool(superToken, members[i]) || members[i] == address(0)) continue; + _helperUpdateMemberUnits(pool, alice, members[i], memberUnits[i], useBools_); + } + + int96 actualDistributionFlowRate; + { + int96 requestedFlowRate = flowRate; + _helperDistributeFlow(superToken, alice, alice, pool, requestedFlowRate, useBools_.useForwarder); + (actualDistributionFlowRate,) = + sf.gda.estimateFlowDistributionActualFlowRate(superToken, alice, pool, requestedFlowRate); + + vm.warp(block.timestamp + warpTime); + } + uint128 totalUnits = pool.getTotalUnits(); + + for (uint256 i; i < members.length; ++i) { + if (members[i] != address(0)) { + // @note we test realtimeBalanceOfNow here as well + (int256 memberRTB,,) = sf.gda.realtimeBalanceOf(superToken, members[i], block.timestamp); + (int256 rtbNow,,,) = sf.gda.realtimeBalanceOfNow(superToken, members[i]); + assertEq(memberRTB, rtbNow, "testDistributeFlowToUnconnectedMembers: rtb != rtbNow"); + + assertEq( + pool.getTotalDisconnectedFlowRate(), + actualDistributionFlowRate, + "testDistributeFlowToUnconnectedMembers: pendingDistributionFlowRate != actualDistributionFlowRate" + ); + (int256 memberClaimable,) = pool.getClaimableNow(members[i]); + + assertEq( + memberClaimable, + totalUnits > 0 + ? (actualDistributionFlowRate * int96(int256(uint256(warpTime)))) * int96(uint96(memberUnits[i])) + / uint256(totalUnits).toInt256() + : int256(0), + "testDistributeFlowToUnconnectedMembers: memberClaimable != (actualDistributionFlowRate * warpTime) / totalUnits" + ); + assertEq(memberRTB, 0, "testDistributeFlowToUnconnectedMembers: memberRTB != 0"); + + vm.startPrank(members[i]); + if (useBools_.useGDA) { + if (useBools_.useForwarder) { + sf.gdaV1Forwarder.claimAll(pool, members[i], new bytes(0)); + } else { + superToken.claimAll(pool, members[i]); + } + } else { + pool.claimAll(); + } + vm.stopPrank(); + + (memberRTB,,) = sf.gda.realtimeBalanceOf(superToken, members[i], block.timestamp); + assertEq( + memberRTB, memberClaimable, "testDistributeFlowToUnconnectedMembers: memberRTB != memberClaimable" + ); + } + } + } + + // Pool ERC20 functions + + function testApproveOnly(address owner, address spender, uint256 amount, PoolConfig memory config) public { + vm.assume(owner != address(0)); + vm.assume(spender != address(0)); + + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + + _helperSuperfluidPoolApprove(pool, owner, spender, amount); + } + + function testIncreaseAllowance(address owner, address spender, uint256 addedValue, PoolConfig memory config) + public + { + vm.assume(owner != address(0)); + vm.assume(spender != address(0)); + + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + + _helperSuperfluidPoolIncreaseAllowance(pool, owner, spender, addedValue); + } + + function testDecreaseAllowance( + address owner, + address spender, + uint256 addedValue, + uint256 subtractedValue, + PoolConfig memory config + ) public { + vm.assume(owner != address(0)); + vm.assume(spender != address(0)); + vm.assume(addedValue >= subtractedValue); + + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config); + + _helperSuperfluidPoolIncreaseAllowance(pool, owner, spender, addedValue); + _helperSuperfluidPoolDecreaseAllowance(pool, owner, spender, subtractedValue); + } + + function testRevertIfUnitsTransferReceiverIsPool(address from, address to, int96 unitsAmount, int128 transferAmount) + public + { + // @note we use int96 because overflow will happen otherwise + vm.assume(unitsAmount >= 0); + vm.assume(transferAmount > 0); + vm.assume(from != address(0)); + vm.assume(to != address(0)); + vm.assume(from != to); + vm.assume(transferAmount <= unitsAmount); + _helperUpdateMemberUnits(freePool, alice, from, uint128(int128(unitsAmount))); + + vm.startPrank(from); + vm.expectRevert(ISuperfluidPool.SUPERFLUID_POOL_NO_POOL_MEMBERS.selector); + freePool.transfer(address(freePool), uint256(uint128(transferAmount))); + vm.stopPrank(); + } + + function testRevertIfTransferNotAllowed(bool useForwarder) public { + PoolConfig memory config = PoolConfig({ transferabilityForUnitsOwner: false, distributionFromAnyAddress: true }); + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, useForwarder, config); + + _helperUpdateMemberUnits(pool, alice, bob, 1000); + + vm.startPrank(bob); + vm.expectRevert(ISuperfluidPool.SUPERFLUID_POOL_TRANSFER_UNITS_NOT_ALLOWED.selector); + pool.transfer(alice, 1000); + vm.stopPrank(); + } + + function testRevertIfTransferFromNotAllowed(bool useForwarder) public { + PoolConfig memory config = PoolConfig({ transferabilityForUnitsOwner: false, distributionFromAnyAddress: true }); + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, useForwarder, config); + + _helperUpdateMemberUnits(freePool, alice, bob, 1000); + + vm.startPrank(bob); + pool.approve(carol, 1000); + vm.stopPrank(); + + vm.startPrank(carol); + vm.expectRevert(ISuperfluidPool.SUPERFLUID_POOL_TRANSFER_UNITS_NOT_ALLOWED.selector); + pool.transferFrom(bob, carol, 1000); + vm.stopPrank(); + } + + function testBasicTransfer( + address from, + address to, + int96 unitsAmount, + int128 transferAmount, + FoundrySuperfluidTester._StackVars_UseBools memory useBools_ + ) public { + // @note we use int96 because overflow will happen otherwise + vm.assume(unitsAmount >= 0); + vm.assume(transferAmount > 0); + vm.assume(from != address(0)); + vm.assume(to != address(0)); + vm.assume(from != to); + vm.assume(transferAmount <= unitsAmount); + _helperUpdateMemberUnits(freePool, alice, from, uint128(int128(unitsAmount)), useBools_); + + _helperSuperfluidPoolUnitsTransfer(freePool, from, to, uint256(uint128(transferAmount))); + } + + function testApproveAndTransferFrom( + address owner, + address spender, + int128 transferAmount, + FoundrySuperfluidTester._StackVars_UseBools memory useBools_ + ) public { + vm.assume(transferAmount > 0); + vm.assume(spender != address(0)); + vm.assume(owner != address(0)); + vm.assume(spender != owner); + _helperUpdateMemberUnits(freePool, alice, owner, uint128(int128(transferAmount)), useBools_); + _helperSuperfluidPoolApprove(freePool, owner, spender, uint256(uint128(transferAmount))); + _helperSuperfluidPoolUnitsTransferFrom(freePool, spender, owner, spender, uint256(uint128(transferAmount))); + } + + function testIncreaseAllowanceAndTransferFrom( + address owner, + address spender, + int128 transferAmount, + FoundrySuperfluidTester._StackVars_UseBools memory useBools_ + ) public { + vm.assume(transferAmount > 0); + vm.assume(spender != address(0)); + vm.assume(owner != address(0)); + vm.assume(spender != owner); + _helperUpdateMemberUnits(freePool, alice, owner, uint128(int128(transferAmount)), useBools_); + _helperSuperfluidPoolIncreaseAllowance(freePool, owner, spender, uint256(uint128(transferAmount))); + _helperSuperfluidPoolUnitsTransferFrom(freePool, spender, owner, spender, uint256(uint128(transferAmount))); + } + + /*////////////////////////////////////////////////////////////////////////// + Assertion Functions + //////////////////////////////////////////////////////////////////////////*/ + + struct PoolUpdateStep { + uint8 u; // which user + uint8 a; // action types: 0 update units, 1 distribute flow, 2 freePool connection, 3 freePool claim for, + // 4 distribute + uint32 v; // action param + uint16 dt; // time delta + } + + function testPoolRandomSeqs(PoolUpdateStep[20] memory steps, _StackVars_UseBools memory useBools_) external { + uint256 N_MEMBERS = 5; + + for (uint256 i = 0; i < steps.length; ++i) { + emit log_named_string("", ""); + emit log_named_uint(">>> STEP", i); + PoolUpdateStep memory s = steps[i]; + uint256 action = s.a % 5; + uint256 u = 1 + s.u % N_MEMBERS; + address user = TEST_ACCOUNTS[u]; + + emit log_named_uint("user", u); + emit log_named_uint("time delta", s.dt); + emit log_named_uint("> timestamp", block.timestamp); + emit log_named_address("tester", user); + + if (action == 0) { + emit log_named_string("action", "updateMember"); + emit log_named_uint("units", s.v); + _helperUpdateMemberUnits(freePool, freePool.admin(), user, s.v, useBools_); + } else if (action == 1) { + emit log_named_string("action", "distributeFlow"); + emit log_named_uint("flow rate", s.v); + if (sf.gda.getFlowRate(superToken, user, freePool) == 0) { + vm.assume(s.v > 0); + } + _helperDistributeFlow(superToken, user, user, freePool, int96(uint96(s.v)), useBools_.useForwarder); + } else if (action == 2) { + address u4 = TEST_ACCOUNTS[1 + (s.v % N_MEMBERS)]; + emit log_named_string("action", "claimAll"); + emit log_named_address("claim for", u4); + vm.startPrank(user); + assert(freePool.claimAll(u4)); + vm.stopPrank(); + } else if (action == 3) { + bool doConnect = s.v % 2 == 0 ? false : true; + emit log_named_string("action", "doConnectPool"); + emit log_named_string("doConnect", doConnect ? "true" : "false"); + doConnect + ? _helperConnectPool(user, superToken, freePool, useBools_.useForwarder) + : _helperDisconnectPool(user, superToken, freePool, useBools_.useForwarder); + } else if (action == 4) { + emit log_named_string("action", "distribute"); + emit log_named_uint("distributionAmount", s.v); + _helperDistributeViaGDA(superToken, user, user, freePool, uint256(s.v), useBools_.useForwarder); + } else { + assert(false); + } + + { + (int256 rtb, uint256 buffer, uint256 owedBuffer) = + sf.gda.realtimeBalanceOf(superToken, address(freePool), block.timestamp); + int96 nr = useBools_.useForwarder + ? sf.gdaV1Forwarder.getNetFlow(superToken, address(freePool)) + : sf.gda.getNetFlow(superToken, address(freePool)); + emit log_string("> freePool before time warp"); + emit log_named_int("rtb", rtb); + emit log_named_uint("buffer", buffer); + emit log_named_uint("owedBuffer", owedBuffer); + emit log_named_int("freePool net flow rate", nr); + } + + emit log_named_uint("> dt", s.dt); + vm.warp(block.timestamp + s.dt); + + { + (int256 rtb, uint256 buffer, uint256 owedBuffer) = + sf.gda.realtimeBalanceOf(superToken, address(freePool), block.timestamp); + int96 nr = useBools_.useForwarder + ? sf.gdaV1Forwarder.getNetFlow(superToken, address(freePool)) + : sf.gda.getNetFlow(superToken, address(freePool)); + emit log_string("> freePool before time warp"); + emit log_named_int("rtb", rtb); + emit log_named_uint("buffer", buffer); + emit log_named_uint("owedBuffer", owedBuffer); + emit log_named_int("freePool net flow rate", nr); + } + } + + int96 flowRatesSum; + { + int96 poolNetFlowRate = sf.gda.getNetFlow(superToken, address(freePool)); + flowRatesSum = flowRatesSum + poolNetFlowRate; + } + + for (uint256 i = 1; i <= N_MEMBERS; ++i) { + int96 flowRate = sf.gda.getNetFlow(superToken, TEST_ACCOUNTS[i]); + flowRatesSum = flowRatesSum + flowRate; + } + + assertEq(flowRatesSum, 0, "GDAv1.t: flowRatesSum != 0"); + } +} diff --git a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreementV1.prop.sol b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreementV1.prop.sol new file mode 100644 index 0000000000..487e50ef95 --- /dev/null +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreementV1.prop.sol @@ -0,0 +1,372 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import { IBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@superfluid-finance/solidity-semantic-money/src/SemanticMoney.sol"; + +import { + ProxyDeployerLibrary, + SuperfluidPoolLogicDeployerLibrary, + SuperfluidUpgradeableBeacon +} from "../../../../contracts/utils/SuperfluidFrameworkDeploymentSteps.sol"; +import { ERC1820RegistryCompiled } from "../../../../contracts/libs/ERC1820RegistryCompiled.sol"; +import { SuperfluidFrameworkDeployer } from "../../../../contracts/utils/SuperfluidFrameworkDeployer.sol"; +import { TestToken } from "../../../../contracts/utils/TestToken.sol"; +import { ISuperToken, SuperToken } from "../../../../contracts/superfluid/SuperToken.sol"; +import { ISuperAgreement } from "../../../../contracts/interfaces/superfluid/ISuperAgreement.sol"; +import { + GeneralDistributionAgreementV1, + ISuperfluid, + ISuperfluidPool +} from "../../../../contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol"; +import { + IGeneralDistributionAgreementV1, + PoolConfig +} from "../../../../contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; +import { ISuperfluidPool, SuperfluidPool } from "../../../../contracts/agreements/gdav1/SuperfluidPool.sol"; +import { SuperTokenV1Library } from "../../../../contracts/apps/SuperTokenV1Library.sol"; + +/// @title GeneralDistributionAgreementV1 Property Tests +/// @author Superfluid +/// @notice This is a contract that runs property tests for the GDAv1 +/// It involves testing the pure functions of the GDAv1 to ensure that we get +/// the expected output for a range of inputs. +contract GeneralDistributionAgreementV1Properties is GeneralDistributionAgreementV1, Test { + using SuperTokenV1Library for ISuperToken; + + SuperfluidFrameworkDeployer internal immutable sfDeployer; + SuperfluidFrameworkDeployer.Framework internal sf; + + SuperfluidPool public currentPool; + uint256 public liquidationPeriod; + + /// @dev The current underlying token being tested (applies only to wrapper super tokens) + TestToken internal token; + + /// @dev The current super token being tested + ISuperToken internal superToken; + + address public constant alice = address(0x420); + + constructor() GeneralDistributionAgreementV1(ISuperfluid(address(0))) { + // deploy ERC1820 registry + vm.etch(ERC1820RegistryCompiled.at, ERC1820RegistryCompiled.bin); + sfDeployer = new SuperfluidFrameworkDeployer(); + sfDeployer.deployTestFramework(); + sf = sfDeployer.getFramework(); + + (token, superToken) = sfDeployer.deployWrapperSuperToken("FTT", "FTT", 18, type(uint256).max, address(0)); + + // /// Deploy SuperfluidPool logic contract + // SuperfluidPool superfluidPoolLogic = + // SuperfluidPoolLogicDeployerLibrary.deploySuperfluidPool(GeneralDistributionAgreementV1(address(this))); + + // // Initialize the logic contract + // superfluidPoolLogic.castrate(); + + // SuperfluidUpgradeableBeacon superfluidPoolBeacon = + // ProxyDeployerLibrary.deploySuperfluidUpgradeableBeacon(address(superfluidPoolLogic)); + // this.initialize(superfluidPoolBeacon); + + PoolConfig memory poolConfig = + PoolConfig({ transferabilityForUnitsOwner: true, distributionFromAnyAddress: true }); + + vm.startPrank(alice); + currentPool = SuperfluidPool(address(sf.gda.createPool(superToken, alice, poolConfig))); + vm.stopPrank(); + + (liquidationPeriod,) = sf.governance.getPPPConfig(sf.host, superToken); + } + + /*////////////////////////////////////////////////////////////////////////// + GDA Setters/Getters Tests + //////////////////////////////////////////////////////////////////////////*/ + // Universal Index Setters/Getters + function testSetGetUIndex(address owner, uint32 settledAt, int96 flowRate, int256 settledValue) public { + bytes memory eff = abi.encode(superToken); + BasicParticle memory p = BasicParticle({ + _settled_at: Time.wrap(settledAt), + _flow_rate: FlowRate.wrap(flowRate), + _settled_value: Value.wrap(settledValue) + }); + _setUIndex(eff, owner, p); + BasicParticle memory setP = _getUIndex(eff, owner); + + assertEq(Time.unwrap(p._settled_at), Time.unwrap(setP._settled_at), "settledAt not equal"); + assertEq(FlowRate.unwrap(p._flow_rate), FlowRate.unwrap(setP._flow_rate), "flowRate not equal"); + assertEq(Value.unwrap(p._settled_value), Value.unwrap(setP._settled_value), "settledValue not equal"); + } + + function testSetGetUIndexData(address owner, uint32 settledAt, int96 flowRate, int256 settledValue) public { + vm.assume(owner != address(currentPool)); + + bytes memory eff = abi.encode(superToken); + BasicParticle memory p = BasicParticle({ + _settled_at: Time.wrap(settledAt), + _flow_rate: FlowRate.wrap(flowRate), + _settled_value: Value.wrap(settledValue) + }); + _setUIndex(eff, owner, p); + GeneralDistributionAgreementV1.UniversalIndexData memory setUIndexData = _getUIndexData(eff, owner); + + assertEq(settledAt, setUIndexData.settledAt, "settledAt not equal"); + assertEq(flowRate, setUIndexData.flowRate, "flowRate not equal"); + assertEq(settledValue, setUIndexData.settledValue, "settledValue not equal"); + assertEq(0, setUIndexData.totalBuffer, "totalBuffer not equal"); + assertEq(false, setUIndexData.isPool, "isPool not equal"); + } + + // Flow Distribution Data Setters/Getters + function testSetGetFlowDistributionData( + address from, + ISuperfluidPool to, + uint32 newFlowRate, + uint96 newFlowRateDelta + ) public { + uint256 lastUpdated = block.timestamp; + + bytes32 flowHash = _getFlowDistributionHash(from, to); + + _setFlowInfo( + abi.encode(superToken), + flowHash, + from, + address(to), + FlowRate.wrap(int128(uint128(newFlowRate))), + FlowRate.wrap(int128(uint128(newFlowRateDelta))) + ); + + vm.warp(1000); + + (bool exist, IGeneralDistributionAgreementV1.FlowDistributionData memory setFlowDistributionData) = + _getFlowDistributionData(superToken, flowHash); + + assertEq(true, exist, "flow distribution data does not exist"); + + assertEq(int96(uint96(newFlowRate)), setFlowDistributionData.flowRate, "flowRate not equal"); + + assertEq(lastUpdated, setFlowDistributionData.lastUpdated, "lastUpdated not equal"); + + assertEq(0, setFlowDistributionData.buffer, "buffer not equal"); + assertEq( + int96(FlowRate.unwrap(_getFlowRate(abi.encode(superToken), flowHash))), + int96(uint96(newFlowRate)), + "_getFlowRate: flow rate not equal" + ); + assertEq( + int96(FlowRate.unwrap(_getFlowRate(abi.encode(superToken), flowHash))), + int96(uint96(newFlowRate)), + "getFlowRate: flow rate not equal" + ); + } + + // Pool Member Data Setters/Getters + function testSetGetPoolMemberData(address poolMember, ISuperfluidPool _pool, uint32 poolID) public { + vm.assume(poolID > 0); + vm.assume(address(_pool) != address(0)); + vm.assume(address(poolMember) != address(0)); + bytes32 poolMemberId = _getPoolMemberHash(poolMember, _pool); + + vm.startPrank(address(this)); + superToken.updateAgreementData( + poolMemberId, + _encodePoolMemberData( + IGeneralDistributionAgreementV1.PoolMemberData({ poolID: poolID, pool: address(_pool) }) + ) + ); + vm.stopPrank(); + + (bool exist, IGeneralDistributionAgreementV1.PoolMemberData memory setPoolMemberData) = + _getPoolMemberData(superToken, poolMember, _pool); + + assertEq(true, exist, "pool member data does not exist"); + assertEq(poolID, setPoolMemberData.poolID, "poolID not equal"); + assertEq(address(_pool), setPoolMemberData.pool, "pool not equal"); + } + + // Proportional Distribution Pool Index Setters/Getters + function testSetGetPDPIndex( + address owner, + uint128 totalUnits, + uint32 wrappedSettledAt, + int96 wrappedFlowRate, + int256 wrappedSettledValue + ) public { + vm.assume(owner != address(0)); + vm.assume(totalUnits < uint128(type(int128).max)); + bytes memory eff = abi.encode(superToken); + PDPoolIndex memory pdpIndex = PDPoolIndex({ + total_units: Unit.wrap(int128(totalUnits)), + _wrapped_particle: BasicParticle({ + _settled_at: Time.wrap(wrappedSettledAt), + _flow_rate: FlowRate.wrap(wrappedFlowRate), + _settled_value: Value.wrap(wrappedSettledValue) + }) + }); + + // we have to pretend to be the registered gda, not this testing contract + vm.startPrank(address(sf.gda)); + _setPDPIndex(eff, address(currentPool), pdpIndex); + vm.stopPrank(); + + (PDPoolIndex memory setPdpIndex) = _getPDPIndex(new bytes(0), address(currentPool)); + + assertEq(Unit.unwrap(pdpIndex.total_units), Unit.unwrap(setPdpIndex.total_units), "total units not equal"); + assertEq( + Time.unwrap(pdpIndex._wrapped_particle._settled_at), + Time.unwrap(setPdpIndex._wrapped_particle._settled_at), + "settled at not equal" + ); + assertEq( + FlowRate.unwrap(pdpIndex._wrapped_particle._flow_rate), + FlowRate.unwrap(setPdpIndex._wrapped_particle._flow_rate), + "flow rate not equal" + ); + assertEq( + Value.unwrap(pdpIndex._wrapped_particle._settled_value), + Value.unwrap(setPdpIndex._wrapped_particle._settled_value), + "settled value not equal" + ); + } + + // // Adjust Buffer => FlowDistributionData modified + // function testAdjustBufferUpdatesFlowDistributionData(address from, int32 oldFlowRate, int32 newFlowRate) public { + // vm.assume(newFlowRate >= 0); + + // bytes32 flowHash = _getFlowDistributionHash(from, currentPool); + + // uint256 expectedBuffer = uint256(int256(newFlowRate)) * liquidationPeriod; + // _adjustBuffer( + // abi.encode(superToken), + // address(currentPool), + // from, + // flowHash, + // FlowRate.wrap(int128(oldFlowRate)), + // FlowRate.wrap(int128(newFlowRate)) + // ); + + // (bool exist, IGeneralDistributionAgreementV1.FlowDistributionData memory flowDistributionData) = + // _getFlowDistributionData(superToken, flowHash); + // assertEq(exist, true, "flow distribution data does not exist"); + // assertEq(flowDistributionData.buffer, expectedBuffer, "buffer not equal"); + // assertEq(flowDistributionData.flowRate, int96(newFlowRate), "buffer not equal"); + // assertEq( + // int96(FlowRate.unwrap(_getFlowRate(abi.encode(superToken), flowHash))), + // int96(newFlowRate), + // "_getFlowRate: flow rate not equal" + // ); + // assertEq( + // sf.gda.getFlowRate(superToken, from, ISuperfluidPool(currentPool)), + // int96(newFlowRate), + // "getFlowRate: flow rate not equal" + // ); + // } + + // // Adjust Buffer => UniversalIndexData modified + // function testAdjustBufferUpdatesUniversalIndexData(address from, int32 oldFlowRate, int32 newFlowRate) public { + // vm.assume(newFlowRate >= 0); + + // uint256 bufferDelta = uint256(int256(newFlowRate)) * liquidationPeriod; // expected buffer == buffer delta + // // because of fresh state + // bytes32 flowHash = _getFlowDistributionHash(from, currentPool); + // GeneralDistributionAgreementV1.UniversalIndexData memory fromUindexDataBefore = + // _getUIndexData(abi.encode(superToken), from); + // _adjustBuffer( + // abi.encode(superToken), + // address(currentPool), + // from, + // flowHash, + // FlowRate.wrap(int128(oldFlowRate)), + // FlowRate.wrap(int128(newFlowRate)) + // ); + + // GeneralDistributionAgreementV1.UniversalIndexData memory fromUindexDataAfter = + // _getUIndexData(abi.encode(superToken), from); + + // assertEq( + // fromUindexDataBefore.totalBuffer + bufferDelta, + // fromUindexDataAfter.totalBuffer, + // "from total buffer not equal" + // ); + // } + + function testEncodeDecodeParticleInputUniversalIndexData( + int96 flowRate, + uint32 settledAt, + int256 settledValue, + uint96 totalBuffer, + bool isPool_ + ) public { + BasicParticle memory particle = BasicParticle({ + _flow_rate: FlowRate.wrap(flowRate), + _settled_at: Time.wrap(settledAt), + _settled_value: Value.wrap(settledValue) + }); + bytes32[] memory encoded = _encodeUniversalIndexData(particle, totalBuffer, isPool_); + (, UniversalIndexData memory decoded) = _decodeUniversalIndexData(encoded); + + assertEq(flowRate, decoded.flowRate, "flowRate not equal"); + assertEq(settledAt, decoded.settledAt, "settledAt not equal"); + assertEq(settledValue, decoded.settledValue, "settledValue not equal"); + assertEq(totalBuffer, decoded.totalBuffer, "totalBuffer not equal"); + assertEq(isPool_, decoded.isPool, "isPool not equal"); + } + + function testEncodeDecodeUIDataInputeUniversalIndexData( + int96 flowRate, + uint32 settledAt, + int256 settledValue, + uint96 totalBuffer, + bool isPool_ + ) public { + UniversalIndexData memory data = UniversalIndexData({ + flowRate: flowRate, + settledAt: settledAt, + settledValue: settledValue, + totalBuffer: totalBuffer, + isPool: isPool_ + }); + + bytes32[] memory encoded = _encodeUniversalIndexData(data); + (, UniversalIndexData memory decoded) = _decodeUniversalIndexData(encoded); + + assertEq(flowRate, decoded.flowRate, "flowRate not equal"); + assertEq(settledAt, decoded.settledAt, "settledAt not equal"); + assertEq(settledValue, decoded.settledValue, "settledValue not equal"); + assertEq(totalBuffer, decoded.totalBuffer, "totalBuffer not equal"); + assertEq(isPool_, decoded.isPool, "isPool not equal"); + } + + function testGetBasicParticleFromUIndex(UniversalIndexData memory data) public { + BasicParticle memory particle = _getBasicParticleFromUIndex(data); + assertEq(data.flowRate, int96(FlowRate.unwrap(particle._flow_rate)), "flowRate not equal"); + assertEq(data.settledAt, Time.unwrap(particle._settled_at), "settledAt not equal"); + assertEq(data.settledValue, Value.unwrap(particle._settled_value), "settledValue not equal"); + } + + function testEncodeDecodeFlowDistributionData(int96 flowRate, uint96 buffer) public { + vm.assume(flowRate >= 0); + vm.assume(buffer >= 0); + IGeneralDistributionAgreementV1.FlowDistributionData memory original = IGeneralDistributionAgreementV1 + .FlowDistributionData({ flowRate: flowRate, lastUpdated: uint32(block.timestamp), buffer: buffer }); + bytes32[] memory encoded = _encodeFlowDistributionData(original); + (, IGeneralDistributionAgreementV1.FlowDistributionData memory decoded) = + _decodeFlowDistributionData(uint256(encoded[0])); + + assertEq(original.flowRate, decoded.flowRate, "flowRate not equal"); + assertEq(original.buffer, decoded.buffer, "buffer not equal"); + assertEq(original.lastUpdated, decoded.lastUpdated, "lastUpdated not equal"); + } + + function testEncodeDecodePoolMemberData(address pool, uint32 poolID) public { + vm.assume(pool != address(0)); + IGeneralDistributionAgreementV1.PoolMemberData memory original = + IGeneralDistributionAgreementV1.PoolMemberData({ pool: pool, poolID: poolID }); + bytes32[] memory encoded = _encodePoolMemberData(original); + (, IGeneralDistributionAgreementV1.PoolMemberData memory decoded) = _decodePoolMemberData(uint256(encoded[0])); + + assertEq(original.pool, decoded.pool, "pool not equal"); + assertEq(original.poolID, decoded.poolID, "poolID not equal"); + } +} diff --git a/packages/ethereum-contracts/test/foundry/apps/CrossStreamSuperApp.t.sol b/packages/ethereum-contracts/test/foundry/apps/CrossStreamSuperApp.t.sol index f92aa8e053..33a68b8572 100644 --- a/packages/ethereum-contracts/test/foundry/apps/CrossStreamSuperApp.t.sol +++ b/packages/ethereum-contracts/test/foundry/apps/CrossStreamSuperApp.t.sol @@ -30,6 +30,13 @@ contract CrossStreamSuperAppTest is FoundrySuperfluidTester { vm.assume(flowRate > 2 ** 32 - 1); int96 initialFlowRate = flowRate; + // @note transfer tokens from alice to carol so that + // alice has type(uint64).max balance to start + uint256 diff = type(uint88).max - type(uint64).max; + vm.startPrank(alice); + superToken.transfer(carol, diff); + vm.stopPrank(); + uint256 balance = superToken.balanceOf(alice); uint256 amountOfTimeTillZero = balance / uint256(uint96(initialFlowRate)); diff --git a/packages/ethereum-contracts/test/foundry/echidna/EchidnaTestCases.t.sol b/packages/ethereum-contracts/test/foundry/echidna/EchidnaTestCases.t.sol new file mode 100644 index 0000000000..813e4487bd --- /dev/null +++ b/packages/ethereum-contracts/test/foundry/echidna/EchidnaTestCases.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import { FoundrySuperfluidTester } from "../FoundrySuperfluidTester.sol"; +import { ISuperfluidPool, SuperfluidPool } from "../../../contracts/agreements/gdav1/SuperfluidPool.sol"; +import { SuperTokenV1Library } from "../../../contracts/apps/SuperTokenV1Library.sol"; +import { ISuperToken, SuperToken } from "../../../contracts/superfluid/SuperToken.sol"; + +/// @dev This contract includes test sequences discovered by echidna which broke invariants previously. +contract EchidnaTestCases is FoundrySuperfluidTester { + using SuperTokenV1Library for ISuperToken; + + SuperfluidPool public currentPool; + + constructor() FoundrySuperfluidTester(6) { } + + function setUp() public override { + super.setUp(); + vm.startPrank(alice); + currentPool = SuperfluidPool(address(superToken.createPool(alice, poolConfig))); + _addAccount(address(currentPool)); + vm.stopPrank(); + } + + function testDistributeFlowToDisconnectedMember(address member, uint64 units, int32 flowRate, bool useForwarder) + public + { + vm.assume(flowRate > 0); + + _helperUpdateMemberUnits(currentPool, alice, member, units); + + _helperDistributeFlow(superToken, alice, alice, currentPool, flowRate, useForwarder); + } + + function testLiquidationCase() public { + int96 flowRate = 28880687301540251; + uint256 warpTime = 70; + + _helperCreateFlow(superToken, alice, bob, flowRate); + _helperTransferAll(superToken, alice, bob); + vm.warp(block.timestamp + warpTime); + _helperDeleteFlow(superToken, carol, alice, bob); + } +} diff --git a/packages/ethereum-contracts/test/foundry/gov/SuperfluidGovernanceII.t.sol b/packages/ethereum-contracts/test/foundry/gov/SuperfluidGovernanceII.t.sol index eefd1fba31..6305d82130 100644 --- a/packages/ethereum-contracts/test/foundry/gov/SuperfluidGovernanceII.t.sol +++ b/packages/ethereum-contracts/test/foundry/gov/SuperfluidGovernanceII.t.sol @@ -8,6 +8,7 @@ import { SuperTokenV1Library } from "../../../contracts/apps/SuperTokenV1Library import { ISuperAgreement } from "../../../contracts/interfaces/superfluid/ISuperAgreement.sol"; import { ISuperfluid } from "../../../contracts/interfaces/superfluid/ISuperfluid.sol"; import { AgreementMock } from "../../../contracts/mocks/AgreementMock.sol"; +import { SuperfluidPool } from "../../../contracts/agreements/gdav1/SuperfluidPool.sol"; contract SuperfluidGovernanceIntegrationTest is FoundrySuperfluidTester { using SuperTokenV1Library for SuperToken; @@ -26,6 +27,7 @@ contract SuperfluidGovernanceIntegrationTest is FoundrySuperfluidTester { function testRevertChangeSuperTokenAdminWhenCallerIsNotNotGovOwner(address newAdmin) public { vm.assume(newAdmin != address(0)); + vm.assume(newAdmin != sf.governance.owner()); vm.startPrank(newAdmin); vm.expectRevert(); @@ -33,7 +35,7 @@ contract SuperfluidGovernanceIntegrationTest is FoundrySuperfluidTester { vm.stopPrank(); } - function testRevertWhenHostIsNotAdmin(address initialAdmin) public { + function testRevertChangeSuperTokenAdminWhenHostIsNotAdmin(address initialAdmin) public { vm.assume(initialAdmin != address(0)); vm.assume(initialAdmin != address(sf.host)); @@ -47,10 +49,50 @@ contract SuperfluidGovernanceIntegrationTest is FoundrySuperfluidTester { vm.stopPrank(); } + function testRevertUpgradePoolBeaconLogicWhenNotOwner() public { + SuperfluidPool newPoolLogic = new SuperfluidPool(sf.gda); + + vm.expectRevert(); + sf.governance.updateContracts(sf.host, address(0), new address[](0), address(0), address(newPoolLogic)); + } + + function testUpdateContractsToUpgradePoolBeaconLogic() public { + SuperfluidPool newPoolLogic = new SuperfluidPool(sf.gda); + vm.startPrank(sf.governance.owner()); + sf.governance.updateContracts(sf.host, address(0), new address[](0), address(0), address(newPoolLogic)); + vm.stopPrank(); + + assertEq( + sf.gda.superfluidPoolBeacon().implementation(), + address(newPoolLogic), + "testUpdateContractsToUpgradePoolBeaconLogic: pool beacon logic not upgraded" + ); + } + + function testRevertUpgradePoolBeaconLogicWhenNotGovernance() public { + SuperfluidPool newPoolLogic = new SuperfluidPool(sf.gda); + vm.expectRevert(); + sf.host.updatePoolBeaconLogic(address(newPoolLogic)); + } + + function testUpgradePoolBeaconLogic() public { + SuperfluidPool newPoolLogic = new SuperfluidPool(sf.gda); + vm.startPrank(address(sf.governance)); + sf.host.updatePoolBeaconLogic(address(newPoolLogic)); + vm.stopPrank(); + + assertEq( + sf.gda.superfluidPoolBeacon().implementation(), + address(newPoolLogic), + "testUpgradePoolBeaconLogic: pool beacon logic not upgraded" + ); + } + function testBatchChangeSuperTokenAdmin(address newAdmin) public { vm.assume(newAdmin != address(0)); - (, ISuperToken localSuperToken) = sfDeployer.deployWrapperSuperToken("FTT", "FTT", 18, type(uint256).max); + (, ISuperToken localSuperToken) = + sfDeployer.deployWrapperSuperToken("FTT", "FTT", 18, type(uint256).max, address(0)); ISuperToken[] memory superTokens = new ISuperToken[](2); superTokens[0] = superToken; // host admin @@ -90,7 +132,8 @@ contract SuperfluidGovernanceIntegrationTest is FoundrySuperfluidTester { vm.assume(newAdmin != address(0)); vm.assume(newAdmin != address(sf.governance.owner())); - (, ISuperToken localSuperToken) = sfDeployer.deployWrapperSuperToken("FTT", "FTT", 18, type(uint256).max); + (, ISuperToken localSuperToken) = + sfDeployer.deployWrapperSuperToken("FTT", "FTT", 18, type(uint256).max, address(0)); ISuperToken[] memory superTokens = new ISuperToken[](2); superTokens[0] = superToken; // host admin diff --git a/packages/ethereum-contracts/test/foundry/libs/SlotsBitmapLibrary.prop.sol b/packages/ethereum-contracts/test/foundry/libs/SlotsBitmapLibrary.prop.sol index 66e093f8e1..81a13cf044 100644 --- a/packages/ethereum-contracts/test/foundry/libs/SlotsBitmapLibrary.prop.sol +++ b/packages/ethereum-contracts/test/foundry/libs/SlotsBitmapLibrary.prop.sol @@ -34,7 +34,7 @@ contract SlotsBitmapLibraryPropertyTest is Test { vm.stopPrank(); vm.startPrank(subscriber); - (token, superToken) = sfDeployer.deployWrapperSuperToken("Test Token", "TST", 18, type(uint256).max); + (token, superToken) = sfDeployer.deployWrapperSuperToken("Test Token", "TST", 18, type(uint256).max, address(0)); vm.stopPrank(); } diff --git a/packages/ethereum-contracts/test/foundry/superfluid/ConstantInflowNFT.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/ConstantInflowNFT.t.sol index c1422b25c9..37e635dec3 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/ConstantInflowNFT.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/ConstantInflowNFT.t.sol @@ -11,148 +11,45 @@ contract ConstantInflowNFTTest is FlowNFTBaseTest { /*////////////////////////////////////////////////////////////////////////// Revert Tests //////////////////////////////////////////////////////////////////////////*/ - function testRevertIfContractAlreadyInitialized() public { - vm.expectRevert("Initializable: contract is already initialized"); - constantInflowNFTProxy.initialize( - string.concat("henlo", INFLOW_NFT_NAME_TEMPLATE), string.concat("goodbye", INFLOW_NFT_SYMBOL_TEMPLATE) - ); - } - - function testRevertIfOwnerOfCalledForNonExistentToken(uint256 _tokenId) public { - vm.expectRevert(IFlowNFTBase.CFA_NFT_INVALID_TOKEN_ID.selector); - constantInflowNFTProxy.ownerOf(_tokenId); - } - - function testRevertIfGetApprovedCalledForNonExistentToken(uint256 _tokenId) public { - vm.expectRevert(IFlowNFTBase.CFA_NFT_INVALID_TOKEN_ID.selector); - constantInflowNFTProxy.getApproved(_tokenId); - } - - function testRevertIfApproveToCallerWhenSetApprovalForAll(address _flowSender, address _flowReceiver) public { - _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); - - uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); - - vm.expectRevert(IFlowNFTBase.CFA_NFT_APPROVE_TO_CALLER.selector); - - vm.prank(_flowReceiver); - constantInflowNFTProxy.setApprovalForAll(_flowReceiver, true); - } - - function testRevertIfApproveToCurrentOwner(address _flowSender, address _flowReceiver) public { - _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); - - uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); - - vm.expectRevert(IFlowNFTBase.CFA_NFT_APPROVE_TO_CURRENT_OWNER.selector); - - vm.prank(_flowReceiver); - constantInflowNFTProxy.approve(_flowReceiver, nftId); + function testRevertIfMintIsNotCalledByOutflowNFT(address caller) public { + _assumeCallerIsNotOtherAddress(caller, address(constantOutflowNFT)); + vm.expectRevert(IConstantInflowNFT.CIF_NFT_ONLY_CONSTANT_OUTFLOW.selector); + constantInflowNFT.mint(address(0), 69); } - function testRevertIfApproveAsNonOwner( - address _flowSender, - address _flowReceiver, - address _approver, - address _approvedAccount - ) public { - _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); - /// @dev _flowReceiver is owner of inflow NFT - vm.assume(_approver != _flowReceiver); - vm.assume(_approvedAccount != _flowReceiver); - - uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); - vm.expectRevert(IFlowNFTBase.CFA_NFT_APPROVE_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector); - vm.prank(_approver); - constantInflowNFTProxy.approve(_approvedAccount, nftId); + function testRevertIfBurnIsNotCalledByOutflowNFT(address caller) public { + _assumeCallerIsNotOtherAddress(caller, address(constantOutflowNFT)); + vm.expectRevert(IConstantInflowNFT.CIF_NFT_ONLY_CONSTANT_OUTFLOW.selector); + constantInflowNFT.burn(69); } function testRevertIfYouTryToTransferInflowNFT(address _flowSender, address _flowReceiver) public { _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); + constantOutflowNFT.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); - _assertEventTransfer(address(constantOutflowNFTProxy), address(0), _flowSender, nftId); - - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); - _assertNFTFlowDataStateIsExpected( - nftId, address(superTokenMock), _flowSender, uint32(block.timestamp), _flowReceiver - ); - - vm.prank(_flowReceiver); - vm.expectRevert(IFlowNFTBase.CFA_NFT_TRANSFER_IS_NOT_ALLOWED.selector); - constantInflowNFTProxy.transferFrom(_flowReceiver, _flowSender, nftId); - - vm.prank(_flowReceiver); - vm.expectRevert(IFlowNFTBase.CFA_NFT_TRANSFER_IS_NOT_ALLOWED.selector); - constantInflowNFTProxy.safeTransferFrom(_flowReceiver, _flowSender, nftId); - - vm.prank(_flowReceiver); + vm.startPrank(_flowReceiver); vm.expectRevert(IFlowNFTBase.CFA_NFT_TRANSFER_IS_NOT_ALLOWED.selector); - constantInflowNFTProxy.safeTransferFrom(_flowReceiver, _flowSender, nftId, "0x"); - } - - function testRevertIfYouAreNotTheOwnerAndTryToTransferInflowNFT(address _flowSender, address _flowReceiver) - public - { - _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); - - uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); - - vm.expectRevert(IFlowNFTBase.CFA_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector); - vm.prank(_flowSender); - constantInflowNFTProxy.transferFrom(_flowReceiver, _flowSender, nftId); - - vm.expectRevert(IFlowNFTBase.CFA_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector); - vm.prank(_flowSender); - constantInflowNFTProxy.safeTransferFrom(_flowReceiver, _flowSender, nftId); - - vm.expectRevert(IFlowNFTBase.CFA_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector); - vm.prank(_flowSender); - constantInflowNFTProxy.safeTransferFrom(_flowReceiver, _flowSender, nftId, "0x"); - } - - function testRevertIfMintIsNotCalledByOutflowNFT(address caller) public { - _assumeCallerIsNotOtherAddress(caller, address(constantOutflowNFTProxy)); - vm.expectRevert(IConstantInflowNFT.CIF_NFT_ONLY_CONSTANT_OUTFLOW.selector); - constantInflowNFTProxy.mint(address(0), 69); - } - - function testRevertIfBurnIsNotCalledByOutflowNFT(address caller) public { - _assumeCallerIsNotOtherAddress(caller, address(constantOutflowNFTProxy)); - vm.expectRevert(IConstantInflowNFT.CIF_NFT_ONLY_CONSTANT_OUTFLOW.selector); - constantInflowNFTProxy.burn(69); + constantInflowNFT.transferFrom(_flowReceiver, _flowSender, nftId); + vm.stopPrank(); } /*////////////////////////////////////////////////////////////////////////// Passing Tests //////////////////////////////////////////////////////////////////////////*/ - function testContractSupportsExpectedInterfaces() public { - assertEq(constantInflowNFTProxy.supportsInterface(type(IERC165).interfaceId), true); - assertEq(constantInflowNFTProxy.supportsInterface(type(IERC721).interfaceId), true); - assertEq(constantInflowNFTProxy.supportsInterface(type(IERC721Metadata).interfaceId), true); - } function testProxiableUUIDIsExpectedValue() public { assertEq( - constantInflowNFTProxy.proxiableUUID(), + constantInflowNFT.proxiableUUID(), keccak256("org.superfluid-finance.contracts.ConstantInflowNFT.implementation") ); } - function testNFTBalanceOfIsAlwaysOne(address _owner) public { - assertEq(constantInflowNFTProxy.balanceOf(_owner), 1); - } - function testConstantInflowNFTIsProperlyInitialized() public { - assertEq(constantInflowNFTProxy.name(), INFLOW_NFT_NAME_TEMPLATE); - assertEq(constantInflowNFTProxy.symbol(), INFLOW_NFT_SYMBOL_TEMPLATE); + assertEq(constantInflowNFT.name(), INFLOW_NFT_NAME_TEMPLATE); + assertEq(constantInflowNFT.symbol(), INFLOW_NFT_SYMBOL_TEMPLATE); } function testFlowDataByTokenIdMint(address _flowSender, address _flowReceiver) public { @@ -160,12 +57,12 @@ contract ConstantInflowNFTTest is FlowNFTBaseTest { uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); + constantOutflowNFT.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); _assertNFTFlowDataStateIsExpected( nftId, address(superTokenMock), _flowSender, uint32(block.timestamp), _flowReceiver ); - IFlowNFTBase.FlowNFTData memory flowData = constantInflowNFTProxy.mockFlowNFTDataByTokenId(nftId); + IFlowNFTBase.FlowNFTData memory flowData = constantInflowNFT.flowDataByTokenId(nftId); assertEq(flowData.flowSender, _flowSender); assertEq(flowData.flowReceiver, _flowReceiver); } @@ -175,9 +72,9 @@ contract ConstantInflowNFTTest is FlowNFTBaseTest { uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - _assertEventTransfer(address(constantInflowNFTProxy), address(0), _flowReceiver, nftId); + _assertEventTransfer(address(constantInflowNFT), address(0), _flowReceiver, nftId); - constantInflowNFTProxy.mockMint(_flowReceiver, nftId); + constantInflowNFT.mockMint(_flowReceiver, nftId); _assertNFTFlowDataStateIsEmpty(nftId); } @@ -186,14 +83,14 @@ contract ConstantInflowNFTTest is FlowNFTBaseTest { _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); + constantOutflowNFT.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); _assertNFTFlowDataStateIsExpected( nftId, address(superTokenMock), _flowSender, uint32(block.timestamp), _flowReceiver ); - _assertEventTransfer(address(constantInflowNFTProxy), _flowReceiver, address(0), nftId); + _assertEventTransfer(address(constantInflowNFT), _flowReceiver, address(0), nftId); - constantInflowNFTProxy.mockBurn(nftId); + constantInflowNFT.mockBurn(nftId); _assertNFTFlowDataStateIsExpected( nftId, address(superTokenMock), _flowSender, uint32(block.timestamp), _flowReceiver @@ -202,30 +99,32 @@ contract ConstantInflowNFTTest is FlowNFTBaseTest { function testApprove(address _flowSender, address _flowReceiver, address _approvedAccount) public + override returns (uint256 nftId) { _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); vm.assume(_flowReceiver != _approvedAccount); nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); + constantOutflowNFT.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); _assertNFTFlowDataStateIsExpected( nftId, address(superTokenMock), _flowSender, uint32(block.timestamp), _flowReceiver ); - _assertEventApproval(address(constantInflowNFTProxy), _flowReceiver, _approvedAccount, nftId); + _assertEventApproval(address(constantInflowNFT), _flowReceiver, _approvedAccount, nftId); - vm.prank(_flowReceiver); - constantInflowNFTProxy.approve(_approvedAccount, nftId); + vm.startPrank(_flowReceiver); + constantInflowNFT.approve(_approvedAccount, nftId); + vm.stopPrank(); - _assertApprovalIsExpected(constantInflowNFTProxy, nftId, _approvedAccount); + _assertApprovalIsExpected(constantInflowNFT, nftId, _approvedAccount); } function testApproveThenBurn(address _flowSender, address _flowReceiver, address _approvedAccount) public { uint256 nftId = testApprove(_flowSender, _flowReceiver, _approvedAccount); - constantInflowNFTProxy.mockBurn(nftId); + constantInflowNFT.mockBurn(nftId); - assertEq(constantInflowNFTProxy.mockGetApproved(nftId), address(0)); + assertEq(constantInflowNFT.mockGetApproved(nftId), address(0)); } function testSetApprovalForAll(address _tokenOwner, address _operator, bool _approved) public { @@ -233,10 +132,10 @@ contract ConstantInflowNFTTest is FlowNFTBaseTest { vm.assume(_tokenOwner != _operator); vm.startPrank(_tokenOwner); - _assertEventApprovalForAll(address(constantInflowNFTProxy), _tokenOwner, _operator, _approved); - constantInflowNFTProxy.setApprovalForAll(_operator, _approved); + _assertEventApprovalForAll(address(constantInflowNFT), _tokenOwner, _operator, _approved); + constantInflowNFT.setApprovalForAll(_operator, _approved); vm.stopPrank(); - _assertOperatorApprovalIsExpected(constantInflowNFTProxy, _tokenOwner, _operator, _approved); + _assertOperatorApprovalIsExpected(constantInflowNFT, _tokenOwner, _operator, _approved); } } diff --git a/packages/ethereum-contracts/test/foundry/superfluid/ConstantOutflowNFT.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/ConstantOutflowNFT.t.sol index 4cb8c346b0..b543431680 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/ConstantOutflowNFT.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/ConstantOutflowNFT.t.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.19; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC165, IERC721, IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; - import { UUPSProxy } from "../../../contracts/upgradability/UUPSProxy.sol"; import { FlowNFTBase, ConstantOutflowNFT, IConstantOutflowNFT @@ -14,7 +13,8 @@ import { FoundrySuperfluidTester, SuperTokenV1Library } from "../FoundrySuperflu import { IFlowNFTBase } from "../../../contracts/interfaces/superfluid/IFlowNFTBase.sol"; import { FlowNFTBaseTest } from "./FlowNFTBase.t.sol"; import { SuperToken, SuperTokenMock } from "../../../contracts/mocks/SuperTokenMock.sol"; -import { ConstantOutflowNFTMock, NoNFTSuperTokenMock } from "../../../contracts/mocks/CFAv1NFTMock.sol"; +import { ConstantOutflowNFTMock } from "../../../contracts/mocks/CFAv1NFTMock.sol"; +import { NoNFTSuperTokenMock } from "../../../contracts/mocks/SuperTokenMock.sol"; import { TestToken } from "../../../contracts/utils/TestToken.sol"; import { SuperTokenV1Library } from "../../../contracts/apps/SuperTokenV1Library.sol"; import { ISuperToken } from "../../../contracts/superfluid/SuperToken.sol"; @@ -27,75 +27,20 @@ contract ConstantOutflowNFTTest is FlowNFTBaseTest { /*////////////////////////////////////////////////////////////////////////// Revert Tests //////////////////////////////////////////////////////////////////////////*/ - function testRevertIfContractAlreadyInitialized() public { - vm.expectRevert("Initializable: contract is already initialized"); - - constantOutflowNFTProxy.initialize( - string.concat("henlo", OUTFLOW_NFT_NAME_TEMPLATE), string.concat("goodbye", OUTFLOW_NFT_SYMBOL_TEMPLATE) - ); - } - - function testRevertIfOwnerOfForNonExistentToken(uint256 _tokenId) public { - vm.expectRevert(IFlowNFTBase.CFA_NFT_INVALID_TOKEN_ID.selector); - constantOutflowNFTProxy.ownerOf(_tokenId); - } - - function testRevertIfGetApprovedForNonExistentToken(uint256 _tokenId) public { - vm.expectRevert(IFlowNFTBase.CFA_NFT_INVALID_TOKEN_ID.selector); - constantOutflowNFTProxy.getApproved(_tokenId); - } - - function testRevertIfSetApprovalForAllOperatorApproveToCaller(address _flowSender, address _flowReceiver) public { - _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); - - uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); - vm.expectRevert(IFlowNFTBase.CFA_NFT_APPROVE_TO_CALLER.selector); - vm.prank(_flowSender); - constantOutflowNFTProxy.setApprovalForAll(_flowSender, true); - } - - function testRevertIfApproveToCurrentOwner(address _flowSender, address _flowReceiver) public { - _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); - - uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); - vm.expectRevert(IFlowNFTBase.CFA_NFT_APPROVE_TO_CURRENT_OWNER.selector); - vm.prank(_flowSender); - constantOutflowNFTProxy.approve(_flowSender, nftId); - } - - function testRevertIfApproveAsNonOwner( - address _flowSender, - address _flowReceiver, - address _approver, - address _approvedAccount - ) public { - _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); - /// @dev _flowSender is owner of outflow NFT - vm.assume(_approver != _flowSender); - vm.assume(_approvedAccount != _flowSender); - - uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); - vm.expectRevert(IFlowNFTBase.CFA_NFT_APPROVE_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector); - vm.prank(_approver); - constantOutflowNFTProxy.approve(_approvedAccount, nftId); - } function testRevertIfInternalMintToZeroAddress(address _flowReceiver) public { uint256 nftId = _helperGetNFTID(address(superTokenMock), address(0), _flowReceiver); vm.expectRevert(); - constantOutflowNFTProxy.mockMint(address(superTokenMock), address(0), _flowReceiver, nftId); + constantOutflowNFT.mockMint(address(superTokenMock), address(0), _flowReceiver, nftId); } function testRevertIfInternalMintTokenThatExists(address _flowSender, address _flowReceiver) public { _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); + constantOutflowNFT.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); vm.expectRevert(); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); + constantOutflowNFT.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); } function testRevertIfInternalMintSameToAndFlowReceiver(address _flowSender) public { @@ -103,113 +48,70 @@ contract ConstantOutflowNFTTest is FlowNFTBaseTest { uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowSender); vm.expectRevert(); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowSender, nftId); - } - - function testRevertIfYouTryToTransferOutflowNFT(address _flowSender, address _flowReceiver) public { - _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); - - uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - - _assertEventTransfer(address(constantOutflowNFTProxy), address(0), _flowSender, nftId); - - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); - _assertNFTFlowDataStateIsExpected( - nftId, address(superTokenMock), _flowSender, uint32(block.timestamp), _flowReceiver - ); - - vm.prank(_flowSender); - vm.expectRevert(IFlowNFTBase.CFA_NFT_TRANSFER_IS_NOT_ALLOWED.selector); - constantOutflowNFTProxy.transferFrom(_flowSender, _flowReceiver, nftId); - - vm.prank(_flowSender); - vm.expectRevert(IFlowNFTBase.CFA_NFT_TRANSFER_IS_NOT_ALLOWED.selector); - constantOutflowNFTProxy.safeTransferFrom(_flowSender, _flowReceiver, nftId); - - vm.prank(_flowSender); - vm.expectRevert(IFlowNFTBase.CFA_NFT_TRANSFER_IS_NOT_ALLOWED.selector); - constantOutflowNFTProxy.safeTransferFrom(_flowSender, _flowReceiver, nftId, "0x"); + constantOutflowNFT.mockMint(address(superTokenMock), _flowSender, _flowSender, nftId); } - function testRevertIfYouAreNotTheOwnerAndTryToTransferOutflowNFT(address _flowSender, address _flowReceiver) - public - { - _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); - - uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - - _assertEventTransfer(address(constantOutflowNFTProxy), address(0), _flowSender, nftId); - - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); - _assertNFTFlowDataStateIsExpected( - nftId, address(superTokenMock), _flowSender, uint32(block.timestamp), _flowReceiver - ); - - vm.expectRevert(IFlowNFTBase.CFA_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector); - vm.prank(_flowReceiver); - constantOutflowNFTProxy.transferFrom(_flowSender, _flowReceiver, nftId); - - vm.expectRevert(IFlowNFTBase.CFA_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector); - vm.prank(_flowReceiver); - constantOutflowNFTProxy.safeTransferFrom(_flowSender, _flowReceiver, nftId); - - vm.expectRevert(IFlowNFTBase.CFA_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector); - vm.prank(_flowReceiver); - constantOutflowNFTProxy.safeTransferFrom(_flowSender, _flowReceiver, nftId, "0x"); - } - - function testRevertIfOnCreateIsNotCalledByCFAv1(address caller) public { + function testRevertIfOnCreateIsNotCalledByFlowAgreement(address caller) public { _assumeCallerIsNotOtherAddress(caller, address(sf.cfa)); + _assumeCallerIsNotOtherAddress(caller, address(sf.gda)); + vm.expectRevert(IConstantOutflowNFT.COF_NFT_ONLY_FLOW_AGREEMENTS.selector); vm.prank(caller); - constantOutflowNFTProxy.onCreate(superToken, address(1), address(2)); + constantOutflowNFT.onCreate(superToken, address(1), address(2)); } - function testRevertIfOnUpdateIsNotCalledByCFAv1(address caller) public { + function testRevertIfOnUpdateIsNotCalledByFlowAgreement(address caller) public { _assumeCallerIsNotOtherAddress(caller, address(sf.cfa)); - vm.prank(caller); + _assumeCallerIsNotOtherAddress(caller, address(sf.gda)); + + vm.startPrank(caller); vm.expectRevert(IConstantOutflowNFT.COF_NFT_ONLY_FLOW_AGREEMENTS.selector); - constantOutflowNFTProxy.onUpdate(superToken, address(1), address(2)); + constantOutflowNFT.onUpdate(superToken, address(1), address(2)); + vm.stopPrank(); } - function testRevertIfOnDeleteIsNotCalledByCFAv1(address caller) public { + function testRevertIfOnDeleteIsNotCalledByFlowAgreement(address caller) public { _assumeCallerIsNotOtherAddress(caller, address(sf.cfa)); + _assumeCallerIsNotOtherAddress(caller, address(sf.gda)); vm.prank(caller); vm.expectRevert(IConstantOutflowNFT.COF_NFT_ONLY_FLOW_AGREEMENTS.selector); - constantOutflowNFTProxy.onDelete(superToken, address(1), address(2)); + constantOutflowNFT.onDelete(superToken, address(1), address(2)); } - function testRevertGetNoFlowTokenURI() public { + function testRevertIfGetNoFlowTokenURI() public { uint256 nftId = _helperGetNFTID(address(superTokenMock), alice, bob); vm.expectRevert(); - constantOutflowNFTProxy.tokenURI(nftId); + constantOutflowNFT.tokenURI(nftId); vm.expectRevert(); - constantInflowNFTProxy.tokenURI(nftId); + constantInflowNFT.tokenURI(nftId); + } + + function testRevertIfYouTryToTransferOutflowNFT(address _flowSender, address _flowReceiver) public { + _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); + + uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); + constantOutflowNFT.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); + + vm.startPrank(_flowSender); + vm.expectRevert(IFlowNFTBase.CFA_NFT_TRANSFER_IS_NOT_ALLOWED.selector); + constantOutflowNFT.transferFrom(_flowSender, _flowReceiver, nftId); + vm.stopPrank(); } /*////////////////////////////////////////////////////////////////////////// Passing Tests //////////////////////////////////////////////////////////////////////////*/ - function testContractSupportsExpectedInterfaces() public { - assertEq(constantOutflowNFTProxy.supportsInterface(type(IERC165).interfaceId), true); - assertEq(constantOutflowNFTProxy.supportsInterface(type(IERC721).interfaceId), true); - assertEq(constantOutflowNFTProxy.supportsInterface(type(IERC721Metadata).interfaceId), true); - } function testProxiableUUIDIsExpectedValue() public { assertEq( - constantOutflowNFTProxy.proxiableUUID(), + constantOutflowNFT.proxiableUUID(), keccak256("org.superfluid-finance.contracts.ConstantOutflowNFT.implementation") ); } - function testNFTBalanceOfIsAlwaysOne(address _owner) public { - assertEq(constantInflowNFTProxy.balanceOf(_owner), 1); - } - function testConstantOutflowNFTIsProperlyInitialized() public { - assertEq(constantOutflowNFTProxy.name(), OUTFLOW_NFT_NAME_TEMPLATE); - assertEq(constantOutflowNFTProxy.symbol(), OUTFLOW_NFT_SYMBOL_TEMPLATE); + assertEq(constantOutflowNFT.name(), OUTFLOW_NFT_NAME_TEMPLATE); + assertEq(constantOutflowNFT.symbol(), OUTFLOW_NFT_SYMBOL_TEMPLATE); } function testInternalMintToken(address _flowSender, address _flowReceiver) public { @@ -217,9 +119,9 @@ contract ConstantOutflowNFTTest is FlowNFTBaseTest { uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - _assertEventTransfer(address(constantOutflowNFTProxy), address(0), _flowSender, nftId); + _assertEventTransfer(address(constantOutflowNFT), address(0), _flowSender, nftId); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); + constantOutflowNFT.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); _assertNFTFlowDataStateIsExpected( nftId, address(superTokenMock), _flowSender, uint32(block.timestamp), _flowReceiver ); @@ -229,43 +131,42 @@ contract ConstantOutflowNFTTest is FlowNFTBaseTest { _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); + constantOutflowNFT.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); _assertNFTFlowDataStateIsExpected( nftId, address(superTokenMock), _flowSender, uint32(block.timestamp), _flowReceiver ); - _assertEventTransfer(address(constantOutflowNFTProxy), _flowSender, address(0), nftId); + _assertEventTransfer(address(constantOutflowNFT), _flowSender, address(0), nftId); - constantOutflowNFTProxy.mockBurn(nftId); + constantOutflowNFT.mockBurn(nftId); _assertNFTFlowDataStateIsEmpty(nftId); } function testApprove(address _flowSender, address _flowReceiver, address _approvedAccount) public + override returns (uint256 nftId) { _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); vm.assume(_flowSender != _approvedAccount); nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - constantOutflowNFTProxy.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); - _assertNFTFlowDataStateIsExpected( - nftId, address(superTokenMock), _flowSender, uint32(block.timestamp), _flowReceiver - ); + constantOutflowNFT.mockMint(address(superTokenMock), _flowSender, _flowReceiver, nftId); - _assertEventApproval(address(constantOutflowNFTProxy), _flowSender, _approvedAccount, nftId); + _assertEventApproval(address(constantOutflowNFT), _flowSender, _approvedAccount, nftId); - vm.prank(_flowSender); - constantOutflowNFTProxy.approve(_approvedAccount, nftId); + vm.startPrank(_flowSender); + constantOutflowNFT.approve(_approvedAccount, nftId); + vm.stopPrank(); - _assertApprovalIsExpected(constantOutflowNFTProxy, nftId, _approvedAccount); + _assertApprovalIsExpected(constantOutflowNFT, nftId, _approvedAccount); } function testApproveThenBurn(address _flowSender, address _flowReceiver, address _approvedAccount) public { uint256 nftId = testApprove(_flowSender, _flowReceiver, _approvedAccount); - constantOutflowNFTProxy.mockBurn(nftId); + constantOutflowNFT.mockBurn(nftId); - assertEq(constantOutflowNFTProxy.mockGetApproved(nftId), address(0)); + assertEq(constantOutflowNFT.mockGetApproved(nftId), address(0)); } function testSetApprovalForAll(address _tokenOwner, address _operator, bool _approved) public { @@ -274,11 +175,11 @@ contract ConstantOutflowNFTTest is FlowNFTBaseTest { vm.startPrank(_tokenOwner); - _assertEventApprovalForAll(address(constantOutflowNFTProxy), _tokenOwner, _operator, _approved); - constantOutflowNFTProxy.setApprovalForAll(_operator, _approved); + _assertEventApprovalForAll(address(constantOutflowNFT), _tokenOwner, _operator, _approved); + constantOutflowNFT.setApprovalForAll(_operator, _approved); vm.stopPrank(); - _assertOperatorApprovalIsExpected(constantOutflowNFTProxy, _tokenOwner, _operator, _approved); + _assertOperatorApprovalIsExpected(constantOutflowNFT, _tokenOwner, _operator, _approved); } function testCreateFlowMintsOutflowAndInflowNFTsAndEmitsTransferEvents() public { @@ -295,8 +196,8 @@ contract ConstantOutflowNFTTest is FlowNFTBaseTest { _helperCreateFlowAndAssertNFTInvariants(flowSender, flowReceiver, flowRate); uint256 nftId = _helperGetNFTID(address(superTokenMock), flowSender, flowReceiver); - _assertEventMetadataUpdate(address(constantOutflowNFTProxy), nftId); - _assertEventMetadataUpdate(address(constantInflowNFTProxy), nftId); + _assertEventMetadataUpdate(address(constantOutflowNFT), nftId); + _assertEventMetadataUpdate(address(constantInflowNFT), nftId); vm.prank(flowSender); superTokenMock.updateFlow(flowReceiver, flowRate + 333); @@ -314,9 +215,9 @@ contract ConstantOutflowNFTTest is FlowNFTBaseTest { uint256 nftId = _helperGetNFTID(address(superTokenMock), flowSender, flowReceiver); - _assertEventTransfer(address(constantInflowNFTProxy), flowReceiver, address(0), nftId); + _assertEventTransfer(address(constantInflowNFT), flowReceiver, address(0), nftId); - _assertEventTransfer(address(constantOutflowNFTProxy), flowSender, address(0), nftId); + _assertEventTransfer(address(constantOutflowNFT), flowSender, address(0), nftId); vm.prank(flowSender); superTokenMock.deleteFlow(flowSender, flowReceiver); @@ -333,7 +234,7 @@ contract ConstantOutflowNFTTest is FlowNFTBaseTest { uint256 nftId = _helperGetNFTID(address(superTokenMock), flowSender, flowReceiver); assertEq( - constantOutflowNFTProxy.tokenURI(nftId), + constantOutflowNFT.tokenURI(nftId), string( abi.encodePacked( "https://nft.superfluid.finance/cfa/v2/getmeta?flowRate=", diff --git a/packages/ethereum-contracts/test/foundry/superfluid/ERC721.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/ERC721.t.sol new file mode 100644 index 0000000000..21951ba2a9 --- /dev/null +++ b/packages/ethereum-contracts/test/foundry/superfluid/ERC721.t.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import { IERC721Metadata } from "@openzeppelin/contracts/interfaces/IERC721Metadata.sol"; +import { FoundrySuperfluidTester } from "../FoundrySuperfluidTester.sol"; +import { ConstantOutflowNFTMock, ConstantInflowNFTMock } from "../../../contracts/mocks/CFAv1NFTMock.sol"; +import { PoolAdminNFTMock, PoolMemberNFTMock } from "../../../contracts/mocks/PoolNFTMock.sol"; +import { ConstantOutflowNFT, IConstantOutflowNFT } from "../../../contracts/superfluid/ConstantOutflowNFT.sol"; +import { ConstantInflowNFT, IConstantInflowNFT } from "../../../contracts/superfluid/ConstantInflowNFT.sol"; +import { TestToken } from "../../../contracts/utils/TestToken.sol"; +import { PoolAdminNFT, IPoolAdminNFT } from "../../../contracts/agreements/gdav1/PoolAdminNFT.sol"; +import { PoolMemberNFT, IPoolMemberNFT } from "../../../contracts/agreements/gdav1/PoolMemberNFT.sol"; +import { UUPSProxy } from "../../../contracts/upgradability/UUPSProxy.sol"; +import { UUPSProxiable } from "../../../contracts/upgradability/UUPSProxiable.sol"; +import { SuperToken, SuperTokenMock } from "../../../contracts/mocks/SuperTokenMock.sol"; + +contract ERC721IntegrationTest is FoundrySuperfluidTester { + string internal constant POOL_MEMBER_NFT_NAME_TEMPLATE = "Pool Member NFT"; + string internal constant POOL_MEMBER_NFT_SYMBOL_TEMPLATE = "PMF"; + string internal constant POOL_ADMIN_NFT_NAME_TEMPLATE = "Pool Admin NFT"; + string internal constant POOL_ADMIN_NFT_SYMBOL_TEMPLATE = "PAF"; + string internal constant OUTFLOW_NFT_NAME_TEMPLATE = "Constant Outflow NFT"; + string internal constant OUTFLOW_NFT_SYMBOL_TEMPLATE = "COF"; + string internal constant INFLOW_NFT_NAME_TEMPLATE = "Constant Inflow NFT"; + string internal constant INFLOW_NFT_SYMBOL_TEMPLATE = "CIF"; + + SuperTokenMock public superTokenMock; + + ConstantOutflowNFTMock public constantOutflowNFTLogic; + ConstantInflowNFTMock public constantInflowNFTLogic; + + ConstantOutflowNFTMock public constantOutflowNFT; + ConstantInflowNFTMock public constantInflowNFT; + + PoolMemberNFTMock public poolMemberNFTLogic; + PoolAdminNFTMock public poolAdminNFTLogic; + + PoolMemberNFTMock public poolMemberNFT; + PoolAdminNFTMock public poolAdminNFT; + + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + event MetadataUpdate(uint256 _tokenId); + + constructor() FoundrySuperfluidTester(5) { } + + function setUp() public virtual override { + super.setUp(); + + // Deploy Flow NFTs + + // deploy outflow NFT contract + UUPSProxy outflowProxy = new UUPSProxy(); + + // deploy inflow NFT contract + UUPSProxy inflowProxy = new UUPSProxy(); + + // we deploy mock NFT contracts for the tests to access internal functions + constantOutflowNFTLogic = new ConstantOutflowNFTMock( + sf.host, + IConstantInflowNFT(address(inflowProxy)) + ); + constantInflowNFTLogic = new ConstantInflowNFTMock( + sf.host, + IConstantOutflowNFT(address(outflowProxy)) + ); + + constantOutflowNFTLogic.castrate(); + constantInflowNFTLogic.castrate(); + + // initialize proxy to point at logic + outflowProxy.initializeProxy(address(constantOutflowNFTLogic)); + + // initialize proxy to point at logic + inflowProxy.initializeProxy(address(constantInflowNFTLogic)); + + constantOutflowNFT = ConstantOutflowNFTMock(address(outflowProxy)); + constantInflowNFT = ConstantInflowNFTMock(address(inflowProxy)); + + constantOutflowNFT.initialize(OUTFLOW_NFT_NAME_TEMPLATE, OUTFLOW_NFT_SYMBOL_TEMPLATE); + + constantInflowNFT.initialize(INFLOW_NFT_NAME_TEMPLATE, INFLOW_NFT_SYMBOL_TEMPLATE); + + // Deploy Pool NFTs + + // deploy pool member NFT contract + UUPSProxy poolMemberProxy = new UUPSProxy(); + + // deploy pool admin NFT contract + UUPSProxy poolAdminProxy = new UUPSProxy(); + + // we deploy mock NFT contracts for the tests to access internal functions + poolMemberNFTLogic = new PoolMemberNFTMock(sf.host); + poolAdminNFTLogic = new PoolAdminNFTMock(sf.host); + + poolMemberNFTLogic.castrate(); + poolAdminNFTLogic.castrate(); + + // initialize proxy to point at logic + poolMemberProxy.initializeProxy(address(poolMemberNFTLogic)); + + // initialize proxy to point at logic + poolAdminProxy.initializeProxy(address(poolAdminNFTLogic)); + + poolMemberNFT = PoolMemberNFTMock(address(poolMemberProxy)); + poolAdminNFT = PoolAdminNFTMock(address(poolAdminProxy)); + + poolMemberNFT.initialize(POOL_MEMBER_NFT_NAME_TEMPLATE, POOL_MEMBER_NFT_SYMBOL_TEMPLATE); + + poolAdminNFT.initialize(POOL_ADMIN_NFT_NAME_TEMPLATE, POOL_ADMIN_NFT_SYMBOL_TEMPLATE); + + // Deploy TestToken + TestToken testTokenMock = new TestToken( + "Mock Test", + "MT", + 18, + 100000000 + ); + + // Deploy SuperToken proxy + UUPSProxy superTokenMockProxy = new UUPSProxy(); + + // deploy super token mock for testing with mock constant outflow/inflow NFTs + SuperTokenMock superTokenMockLogic = new SuperTokenMock( + sf.host, + 0, + IConstantOutflowNFT(address(constantOutflowNFT)), + IConstantInflowNFT(address(constantInflowNFT)), + IPoolAdminNFT(address(poolAdminNFT)), + IPoolMemberNFT(address(poolMemberNFT)) + ); + superTokenMockProxy.initializeProxy(address(superTokenMockLogic)); + + superTokenMock = SuperTokenMock(address(superTokenMockProxy)); + superTokenMock.initialize(testTokenMock, 18, "Super Mock Test", "MTx"); + + // mint tokens to test accounts + for (uint256 i = 0; i < N_TESTERS; i++) { + superTokenMock.mintInternal(TEST_ACCOUNTS[i], INIT_SUPER_TOKEN_BALANCE, "0x", "0x"); + } + } + + // If we properly create mock contracts for the base NFT contracts + // then we can just use the base NFT contracts for testing these reverts + // and the other functionality of ERC721 here + // Instead of testing each of the NFT contracts separately + function _helperRevertIfOwnerOf(IERC721Metadata _nftContract, uint256 _tokenId, bytes4 _errorSelector) internal { + vm.expectRevert(_errorSelector); + _nftContract.ownerOf(_tokenId); + } + + function _helperRevertIfGetApproved(IERC721Metadata _nftContract, uint256 _tokenId, bytes4 _errorSelector) + internal + { + vm.expectRevert(_errorSelector); + _nftContract.getApproved(_tokenId); + } + + function _helperRevertIfTransferFrom( + IERC721Metadata _nftContract, + address _caller, + address _from, + address _to, + uint256 _tokenId, + bytes4 _errorSelector + ) internal { + vm.startPrank(_caller); + vm.expectRevert(_errorSelector); + _nftContract.transferFrom(_from, _to, _tokenId); + vm.stopPrank(); + } + + function _helperRevertIfSafeTransferFrom( + IERC721Metadata _nftContract, + address _caller, + address _from, + address _to, + uint256 _tokenId, + bytes4 _errorSelector + ) internal { + vm.startPrank(_caller); + vm.expectRevert(_errorSelector); + _nftContract.safeTransferFrom(_from, _to, _tokenId); + vm.stopPrank(); + } + + function _helperRevertIfSafeTransferFrom( + IERC721Metadata _nftContract, + address _caller, + address _from, + address _to, + uint256 _tokenId, + bytes memory _data, + bytes4 _errorSelector + ) internal { + vm.startPrank(_caller); + vm.expectRevert(_errorSelector); + _nftContract.safeTransferFrom(_from, _to, _tokenId, _data); + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////////////////// + Assertion Helpers + //////////////////////////////////////////////////////////////////////////*/ + function _assertOwnerOfIsExpected( + IERC721Metadata _nftContract, + uint256 _tokenId, + address _expectedOwner, + string memory _message + ) public { + // we use mockOwnerOf to overcome the CFA_NFT_INVALID_TOKEN_ID error + address owner = PoolAdminNFTMock(address(_nftContract)).mockOwnerOf(_tokenId); + + assertEq(owner, _expectedOwner, _message); + } + + function _assertApprovalIsExpected(IERC721Metadata _nftContract, uint256 _tokenId, address _expectedApproved) + public + { + address approved = _nftContract.getApproved(_tokenId); + + assertEq(approved, _expectedApproved); + } + + function _assertOperatorApprovalIsExpected( + IERC721Metadata _nftContract, + address _expectedOwner, + address _expectedOperator, + bool _expectedOperatorApproval + ) public { + bool operatorApproval = _nftContract.isApprovedForAll(_expectedOwner, _expectedOperator); + + assertEq(operatorApproval, _expectedOperatorApproval); + } + + function _assertEventTransfer( + address _emittingAddress, + address _expectedFrom, + address _expectedTo, + uint256 _expectedTokenId + ) public { + vm.expectEmit(true, true, true, false, _emittingAddress); + + emit Transfer(_expectedFrom, _expectedTo, _expectedTokenId); + } + + function _assertEventApproval( + address _emittingAddress, + address _expectedOwner, + address _expectedApproved, + uint256 _expectedTokenId + ) public { + vm.expectEmit(true, true, true, false, _emittingAddress); + + emit Approval(_expectedOwner, _expectedApproved, _expectedTokenId); + } + + function _assertEventApprovalForAll( + address _emittingAddress, + address _expectedOwner, + address _expectedOperator, + bool _expectedApproved + ) public { + vm.expectEmit(true, true, false, true, _emittingAddress); + + emit ApprovalForAll(_expectedOwner, _expectedOperator, _expectedApproved); + } + + function _assertEventMetadataUpdate(address _emittingAddress, uint256 _tokenId) public { + vm.expectEmit(true, false, false, false, _emittingAddress); + + emit MetadataUpdate(_tokenId); + } +} diff --git a/packages/ethereum-contracts/test/foundry/superfluid/FlowNFTBase.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/FlowNFTBase.t.sol index 3c25f69be8..051dd52ebe 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/FlowNFTBase.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/FlowNFTBase.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: AGPLv3 pragma solidity 0.8.19; +import { IERC165, IERC721, IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { UUPSProxy } from "../../../contracts/upgradability/UUPSProxy.sol"; import { UUPSProxiable } from "../../../contracts/upgradability/UUPSProxiable.sol"; import { @@ -10,106 +12,244 @@ import { IConstantOutflowNFT } from "../../../contracts/superfluid/ConstantOutflowNFT.sol"; import { ConstantInflowNFT, IConstantInflowNFT } from "../../../contracts/superfluid/ConstantInflowNFT.sol"; +import { IPoolAdminNFT } from "../../../contracts/agreements/gdav1/PoolAdminNFT.sol"; +import { IPoolMemberNFT } from "../../../contracts/agreements/gdav1/PoolMemberNFT.sol"; import { SuperTokenV1Library } from "../../../contracts/apps/SuperTokenV1Library.sol"; import { FoundrySuperfluidTester } from "../FoundrySuperfluidTester.sol"; import { ConstantOutflowNFTMock, ConstantInflowNFTMock } from "../../../contracts/mocks/CFAv1NFTMock.sol"; import { SuperToken, SuperTokenMock } from "../../../contracts/mocks/SuperTokenMock.sol"; +import { FlowNFTBaseMock } from "../../../contracts/mocks/CFAv1NFTMock.sol"; import { TestToken } from "../../../contracts/utils/TestToken.sol"; import { FlowNFTBaseStorageLayoutMock, ConstantInflowNFTStorageLayoutMock, ConstantOutflowNFTStorageLayoutMock } from "../../../contracts/mocks/CFAv1NFTUpgradabilityMock.sol"; +import { ERC721IntegrationTest } from "./ERC721.t.sol"; -abstract contract FlowNFTBaseTest is FoundrySuperfluidTester { +abstract contract FlowNFTBaseTest is ERC721IntegrationTest { + using Strings for uint256; using SuperTokenV1Library for SuperTokenMock; using SuperTokenV1Library for SuperToken; - string constant internal OUTFLOW_NFT_NAME_TEMPLATE = "Constant Outflow NFT"; - string constant internal OUTFLOW_NFT_SYMBOL_TEMPLATE = "COF"; - string constant internal INFLOW_NFT_NAME_TEMPLATE = "Constant Inflow NFT"; - string constant internal INFLOW_NFT_SYMBOL_TEMPLATE = "CIF"; + string public constant NAME = "Flow NFT Base"; + string public constant SYMBOL = "FNFTB"; - SuperTokenMock public superTokenMock; + FlowNFTBaseMock public flowNFTBaseMock; - ConstantOutflowNFTMock public constantOutflowNFTLogic; - ConstantInflowNFTMock public constantInflowNFTLogic; + function setUp() public virtual override { + super.setUp(); + flowNFTBaseMock = new FlowNFTBaseMock(sf.host); + flowNFTBaseMock.initialize(NAME, SYMBOL); + } - ConstantOutflowNFTMock public constantOutflowNFTProxy; - ConstantInflowNFTMock public constantInflowNFTProxy; + /*////////////////////////////////////////////////////////////////////////// + Revert Tests + //////////////////////////////////////////////////////////////////////////*/ - event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + function testRevertIfContractAlreadyInitialized() public { + vm.expectRevert("Initializable: contract is already initialized"); - event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + flowNFTBaseMock.initialize(NAME, SYMBOL); + } - event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + function testRevertIfOwnerOfCalledForNonExistentToken(uint256 tokenId) public { + _helperRevertIfOwnerOf(flowNFTBaseMock, tokenId, IFlowNFTBase.CFA_NFT_INVALID_TOKEN_ID.selector); + } - event MetadataUpdate(uint256 _tokenId); + function testRevertIfGetApprovedCalledForNonExistentToken(uint256 tokenId) public { + _helperRevertIfGetApproved(flowNFTBaseMock, tokenId, IFlowNFTBase.CFA_NFT_INVALID_TOKEN_ID.selector); + } - constructor() FoundrySuperfluidTester(5) { } + function testRevertIfSetApprovalForAllOperatorApproveToCaller(address _flowSender) public { + vm.assume(_flowSender != address(0)); - function setUp() public virtual override { - super.setUp(); + vm.startPrank(_flowSender); + vm.expectRevert(IFlowNFTBase.CFA_NFT_APPROVE_TO_CALLER.selector); + flowNFTBaseMock.setApprovalForAll(_flowSender, true); + vm.stopPrank(); + } - // deploy outflow NFT contract - UUPSProxy outflowProxy = new UUPSProxy(); + function testRevertIfApproveToCurrentOwner(address _flowSender, address _flowReceiver) public { + _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); - // deploy inflow NFT contract - UUPSProxy inflowProxy = new UUPSProxy(); + uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); + flowNFTBaseMock.mockMint(address(superTokenMock), _flowSender, _flowReceiver); - // we deploy mock NFT contracts for the tests to access internal functions - constantOutflowNFTLogic = new ConstantOutflowNFTMock( - sf.host, - IConstantInflowNFT(address(inflowProxy)) + vm.startPrank(_flowSender); + vm.expectRevert(IFlowNFTBase.CFA_NFT_APPROVE_TO_CURRENT_OWNER.selector); + flowNFTBaseMock.approve(_flowSender, nftId); + vm.stopPrank(); + } + + function testRevertIfApproveAsNonOwner( + address _flowSender, + address _flowReceiver, + address _approver, + address _approvedAccount + ) public { + _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); + /// @dev _flowSender is owner of outflow NFT + vm.assume(_approver != _flowSender); + vm.assume(_approvedAccount != _flowSender); + + uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); + flowNFTBaseMock.mockMint(address(superTokenMock), _flowSender, _flowReceiver); + vm.expectRevert(IFlowNFTBase.CFA_NFT_APPROVE_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector); + vm.startPrank(_approver); + flowNFTBaseMock.approve(_approvedAccount, nftId); + vm.stopPrank(); + } + + function testRevertIfTransferFrom(address _flowSender, address _flowReceiver) public { + _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); + uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); + flowNFTBaseMock.mockMint(address(superTokenMock), _flowSender, _flowReceiver); + + _helperRevertIfTransferFrom( + flowNFTBaseMock, + _flowSender, + _flowSender, + _flowReceiver, + nftId, + IFlowNFTBase.CFA_NFT_TRANSFER_IS_NOT_ALLOWED.selector ); - constantInflowNFTLogic = new ConstantInflowNFTMock( - sf.host, - IConstantOutflowNFT(address(outflowProxy)) + } + + function testRevertIfSafeTransferFrom(address _flowSender, address _flowReceiver) public { + _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); + uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); + flowNFTBaseMock.mockMint(address(superTokenMock), _flowSender, _flowReceiver); + + _helperRevertIfSafeTransferFrom( + flowNFTBaseMock, + _flowSender, + _flowSender, + _flowReceiver, + nftId, + IFlowNFTBase.CFA_NFT_TRANSFER_IS_NOT_ALLOWED.selector ); + } - constantOutflowNFTLogic.castrate(); - constantInflowNFTLogic.castrate(); + function testRevertIfTransferFromWithData(address _flowSender, address _flowReceiver) public { + _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); + uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); + flowNFTBaseMock.mockMint(address(superTokenMock), _flowSender, _flowReceiver); + + _helperRevertIfSafeTransferFrom( + flowNFTBaseMock, + _flowSender, + _flowSender, + _flowReceiver, + nftId, + "0x", + IFlowNFTBase.CFA_NFT_TRANSFER_IS_NOT_ALLOWED.selector + ); + } - // initialize proxy to point at logic - outflowProxy.initializeProxy(address(constantOutflowNFTLogic)); + function testRevertIfTransferFromAsNonOwner(address _flowSender, address _flowReceiver) public { + _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); + uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); + flowNFTBaseMock.mockMint(address(superTokenMock), _flowSender, _flowReceiver); + + _helperRevertIfTransferFrom( + flowNFTBaseMock, + _flowReceiver, + _flowSender, + _flowReceiver, + nftId, + IFlowNFTBase.CFA_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector + ); + } - // initialize proxy to point at logic - inflowProxy.initializeProxy(address(constantInflowNFTLogic)); + function testRevertIfSafeTransferFromAsNonOwner(address _flowSender, address _flowReceiver) public { + _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); + uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); + flowNFTBaseMock.mockMint(address(superTokenMock), _flowSender, _flowReceiver); + + _helperRevertIfSafeTransferFrom( + flowNFTBaseMock, + _flowReceiver, + _flowSender, + _flowReceiver, + nftId, + IFlowNFTBase.CFA_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector + ); + } - constantOutflowNFTProxy = ConstantOutflowNFTMock(address(outflowProxy)); - constantInflowNFTProxy = ConstantInflowNFTMock(address(inflowProxy)); + function testRevertIfTransferFromWithDataAsNonOwner(address _flowSender, address _flowReceiver) public { + _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); + uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); + flowNFTBaseMock.mockMint(address(superTokenMock), _flowSender, _flowReceiver); + + _helperRevertIfSafeTransferFrom( + flowNFTBaseMock, + _flowReceiver, + _flowSender, + _flowReceiver, + nftId, + "0x", + IFlowNFTBase.CFA_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector + ); + } - constantOutflowNFTProxy.initialize("Constant Outflow NFT", "COF"); + /*////////////////////////////////////////////////////////////////////////// + Passing Tests + //////////////////////////////////////////////////////////////////////////*/ + function testContractSupportsExpectedInterfaces() public { + assertEq(flowNFTBaseMock.supportsInterface(type(IERC165).interfaceId), true); + assertEq(flowNFTBaseMock.supportsInterface(type(IERC721).interfaceId), true); + assertEq(flowNFTBaseMock.supportsInterface(type(IERC721Metadata).interfaceId), true); + } - constantInflowNFTProxy.initialize("Constant Inflow NFT", "CIF"); + function testNFTBalanceOfIsAlwaysOne(address _owner) public { + assertEq(flowNFTBaseMock.balanceOf(_owner), 1); + } - // Deploy TestToken - TestToken testTokenMock = new TestToken( - "Mock Test", - "MT", - 18, - 100000000 - ); + function testHostIsProperlySetInConstructor() public { + assertEq(address(flowNFTBaseMock.HOST()), address(sf.host)); + } - // Deploy SuperToken proxy - UUPSProxy superTokenMockProxy = new UUPSProxy(); + function testCFAv1IsProperlySetInConstructor() public { + assertEq(address(flowNFTBaseMock.CONSTANT_FLOW_AGREEMENT_V1()), address(sf.cfa)); + } - // deploy super token mock for testing with mock constant outflow/inflow NFTs - SuperTokenMock superTokenMockLogic = new SuperTokenMock( - sf.host, - 0, - IConstantOutflowNFT(address(constantOutflowNFTProxy)), - IConstantInflowNFT(address(constantInflowNFTProxy)) - ); - superTokenMockProxy.initializeProxy(address(superTokenMockLogic)); + function testGDAv1IsProperlySetInConstructor() public { + assertEq(address(flowNFTBaseMock.GENERAL_DISTRIBUTION_AGREEMENT_V1()), address(sf.gda)); + } - superTokenMock = SuperTokenMock(address(superTokenMockProxy)); - superTokenMock.initialize(testTokenMock, 18, "Super Mock Test", "MTx"); + function testNFTMetadataIsProperlyInitialized() public { + assertEq(flowNFTBaseMock.name(), NAME); + assertEq(flowNFTBaseMock.symbol(), SYMBOL); + } - // mint tokens to test accounts - for (uint256 i = 0; i < N_TESTERS; i++) { - superTokenMock.mintInternal(TEST_ACCOUNTS[i], INIT_SUPER_TOKEN_BALANCE, "0x", "0x"); - } + function testTriggerMetadataUpdate(uint256 tokenId) public { + _assertEventMetadataUpdate(address(flowNFTBaseMock), tokenId); + flowNFTBaseMock.triggerMetadataUpdate(tokenId); + } + + function testTokenURI(uint256 tokenId) public { + assertEq(flowNFTBaseMock.tokenURI(tokenId), string(abi.encodePacked("tokenId=", tokenId.toString()))); + } + + function testApprove(address _flowSender, address _flowReceiver, address _approvedAccount) + public + virtual + returns (uint256 nftId) + { + _assumeSenderNEQReceiverAndNeitherAreZeroAddress(_flowSender, _flowReceiver); + vm.assume(_flowSender != _approvedAccount); + + nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); + flowNFTBaseMock.mockMint(address(superTokenMock), _flowSender, _flowReceiver); + + _assertEventApproval(address(flowNFTBaseMock), _flowSender, _approvedAccount, nftId); + + vm.startPrank(_flowSender); + flowNFTBaseMock.approve(_approvedAccount, nftId); + vm.stopPrank(); + + _assertApprovalIsExpected(flowNFTBaseMock, nftId, _approvedAccount); } /*////////////////////////////////////////////////////////////////////////// @@ -122,7 +262,7 @@ abstract contract FlowNFTBaseTest is FoundrySuperfluidTester { uint32 _expectedFlowStartDate, address _expectedFlowReceiver ) public { - FlowNFTBase.FlowNFTData memory flowData = constantOutflowNFTProxy.flowDataByTokenId(_tokenId); + FlowNFTBase.FlowNFTData memory flowData = constantOutflowNFT.flowDataByTokenId(_tokenId); assertEq(flowData.superToken, _expectedSuperToken); @@ -136,82 +276,20 @@ abstract contract FlowNFTBaseTest is FoundrySuperfluidTester { assertEq(flowData.flowReceiver, _expectedFlowReceiver); // assert owner of outflow nft equal to expected flow sender - _assertOwnerOf(constantOutflowNFTProxy, _tokenId, _expectedFlowSender, true); + _assertOwnerOfIsExpected( + constantOutflowNFT, _tokenId, _expectedFlowSender, "ConstantOutflowNFT: owner of COF nft not as expected" + ); // assert owner of inflow nft equal to expected flow receiver - _assertOwnerOf(constantInflowNFTProxy, _tokenId, _expectedFlowReceiver, false); + _assertOwnerOfIsExpected( + constantInflowNFT, _tokenId, _expectedFlowReceiver, "ConstantInflowNFT: owner of COF nft not as expected" + ); } function _assertNFTFlowDataStateIsEmpty(uint256 _tokenId) public { _assertNFTFlowDataStateIsExpected(_tokenId, address(0), address(0), 0, address(0)); } - function _assertOwnerOf(FlowNFTBase _nftContract, uint256 _tokenId, address _expectedOwner, bool _isOutflow) - public - { - address actualOwner = _isOutflow - ? ConstantOutflowNFTMock(address(_nftContract)).mockOwnerOf(_tokenId) - : ConstantInflowNFTMock(address(_nftContract)).mockOwnerOf(_tokenId); - - assertEq(actualOwner, _expectedOwner); - } - - function _assertApprovalIsExpected(FlowNFTBase _nftContract, uint256 _tokenId, address _expectedApproved) public { - address approved = _nftContract.getApproved(_tokenId); - - assertEq(approved, _expectedApproved); - } - - function _assertOperatorApprovalIsExpected( - FlowNFTBase _nftContract, - address _expectedOwner, - address _expectedOperator, - bool _expectedOperatorApproval - ) public { - bool operatorApproval = _nftContract.isApprovedForAll(_expectedOwner, _expectedOperator); - - assertEq(operatorApproval, _expectedOperatorApproval); - } - - function _assertEventTransfer( - address _emittingAddress, - address _expectedFrom, - address _expectedTo, - uint256 _expectedTokenId - ) public { - vm.expectEmit(true, true, true, false, _emittingAddress); - - emit Transfer(_expectedFrom, _expectedTo, _expectedTokenId); - } - - function _assertEventApproval( - address _emittingAddress, - address _expectedOwner, - address _expectedApproved, - uint256 _expectedTokenId - ) public { - vm.expectEmit(true, true, true, false, _emittingAddress); - - emit Approval(_expectedOwner, _expectedApproved, _expectedTokenId); - } - - function _assertEventApprovalForAll( - address _emittingAddress, - address _expectedOwner, - address _expectedOperator, - bool _expectedApproved - ) public { - vm.expectEmit(true, true, false, true, _emittingAddress); - - emit ApprovalForAll(_expectedOwner, _expectedOperator, _expectedApproved); - } - - function _assertEventMetadataUpdate(address _emittingAddress, uint256 _tokenId) public { - vm.expectEmit(true, false, false, false, _emittingAddress); - - emit MetadataUpdate(_tokenId); - } - /*////////////////////////////////////////////////////////////////////////// Helper Functions //////////////////////////////////////////////////////////////////////////*/ @@ -220,7 +298,7 @@ abstract contract FlowNFTBaseTest is FoundrySuperfluidTester { view returns (uint256) { - return constantOutflowNFTProxy.getTokenId(_superToken, _flowSender, _flowReceiver); + return constantOutflowNFT.getTokenId(_superToken, _flowSender, _flowReceiver); } function _helperCreateFlowAndAssertNFTInvariants(address _flowSender, address _flowReceiver, int96 _flowRate) @@ -228,9 +306,9 @@ abstract contract FlowNFTBaseTest is FoundrySuperfluidTester { { uint256 nftId = _helperGetNFTID(address(superTokenMock), _flowSender, _flowReceiver); - _assertEventTransfer(address(constantOutflowNFTProxy), address(0), _flowSender, nftId); + _assertEventTransfer(address(constantOutflowNFT), address(0), _flowSender, nftId); - _assertEventTransfer(address(constantInflowNFTProxy), address(0), _flowReceiver, nftId); + _assertEventTransfer(address(constantInflowNFT), address(0), _flowReceiver, nftId); vm.startPrank(_flowSender); superTokenMock.createFlow(_flowReceiver, _flowRate); @@ -256,55 +334,40 @@ abstract contract FlowNFTBaseTest is FoundrySuperfluidTester { function _assumeCallerIsNotOtherAddress(address caller, address otherAddress) public pure { vm.assume(caller != otherAddress); } - - /*////////////////////////////////////////////////////////////////////////// - Passing Tests - //////////////////////////////////////////////////////////////////////////*/ - function testCFAv1IsProperlySetDuringInitialization() public { - assertEq(address(constantOutflowNFTProxy.CONSTANT_FLOW_AGREEMENT_V1()), address(sf.cfa)); - assertEq(address(constantInflowNFTProxy.CONSTANT_FLOW_AGREEMENT_V1()), address(sf.cfa)); - } } -/// @title ConstantFAv1NFTsUpgradabilityTest +/// @title CFAv1NFTUpgradabilityTest /// @author Superfluid -/// @notice Used for testing storage layout of CFAv1 NFT contracts -contract ConstantFAv1NFTsUpgradabilityTest is FlowNFTBaseTest { +/// @notice Used for testing storage layout and upgradability of CFAv1 NFT contracts +contract CFAv1NFTUpgradabilityTest is FlowNFTBaseTest { function setUp() public override { super.setUp(); } - /*////////////////////////////////////////////////////////////////////////// - Assertion Helpers - //////////////////////////////////////////////////////////////////////////*/ - function _assertExpectedLogicContractAddress(UUPSProxiable _proxy, address _expectedLogicContract) public { - assertEq(_proxy.getCodeAddress(), _expectedLogicContract); - } - /*////////////////////////////////////////////////////////////////////////// Storage Layout Tests //////////////////////////////////////////////////////////////////////////*/ - function testStorageLayoutOfFlowNFTBase() public { + function testFlowNFTBaseStorageLayout() public { FlowNFTBaseStorageLayoutMock flowNFTBaseStorageLayoutMock = new FlowNFTBaseStorageLayoutMock( sf.host ); flowNFTBaseStorageLayoutMock.validateStorageLayout(); } - function testStorageLayoutOfConstantInflowNFT() public { + function testConstantInflowNFTStorageLayout() public { ConstantInflowNFTStorageLayoutMock constantInflowNFTBaseStorageLayoutMock = new ConstantInflowNFTStorageLayoutMock( sf.host, - constantOutflowNFTProxy + constantOutflowNFT ); constantInflowNFTBaseStorageLayoutMock.validateStorageLayout(); } - function testStorageLayoutOfConstantOutflowNFT() public { + function testConstantOutflowNFTStorageLayout() public { ConstantOutflowNFTStorageLayoutMock constantOutflowNFTBaseStorageLayoutMock = new ConstantOutflowNFTStorageLayoutMock( sf.host, - constantInflowNFTProxy + constantInflowNFT ); constantOutflowNFTBaseStorageLayoutMock.validateStorageLayout(); } @@ -312,41 +375,41 @@ contract ConstantFAv1NFTsUpgradabilityTest is FlowNFTBaseTest { /*////////////////////////////////////////////////////////////////////////// Revert Tests //////////////////////////////////////////////////////////////////////////*/ - function testRevertNFTContractsCannotBeUpgradedByNonHost(address notSuperTokenFactory) public { + function testRevertFlowNFTContractsCannotBeUpgradedByNonSuperTokenFactory(address notSuperTokenFactory) public { vm.assume(notSuperTokenFactory != address(sf.superTokenFactory)); ConstantOutflowNFT newOutflowLogic = new ConstantOutflowNFT( sf.host, - constantInflowNFTProxy + constantInflowNFT ); vm.expectRevert(IFlowNFTBase.CFA_NFT_ONLY_SUPER_TOKEN_FACTORY.selector); vm.prank(notSuperTokenFactory); - constantOutflowNFTProxy.updateCode(address(newOutflowLogic)); + constantOutflowNFT.updateCode(address(newOutflowLogic)); ConstantInflowNFT newInflowLogic = new ConstantInflowNFT( sf.host, - constantOutflowNFTProxy + constantOutflowNFT ); vm.expectRevert(IFlowNFTBase.CFA_NFT_ONLY_SUPER_TOKEN_FACTORY.selector); vm.prank(notSuperTokenFactory); - constantInflowNFTProxy.updateCode(address(newInflowLogic)); + constantInflowNFT.updateCode(address(newInflowLogic)); } /*////////////////////////////////////////////////////////////////////////// Passing Tests //////////////////////////////////////////////////////////////////////////*/ - function testNFTContractsCanBeUpgradedByHost() public { + function testFlowNFTContractsCanBeUpgradedBySuperTokenFactory() public { ConstantOutflowNFT newOutflowLogic = new ConstantOutflowNFT( sf.host, - constantInflowNFTProxy + constantInflowNFT ); vm.prank(address(sf.superTokenFactory)); - constantOutflowNFTProxy.updateCode(address(newOutflowLogic)); + constantOutflowNFT.updateCode(address(newOutflowLogic)); ConstantInflowNFT newInflowLogic = new ConstantInflowNFT( sf.host, - constantOutflowNFTProxy + constantOutflowNFT ); vm.prank(address(sf.superTokenFactory)); - constantInflowNFTProxy.updateCode(address(newInflowLogic)); + constantInflowNFT.updateCode(address(newInflowLogic)); } } diff --git a/packages/ethereum-contracts/test/foundry/superfluid/PoolAdminNFT.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/PoolAdminNFT.t.sol new file mode 100644 index 0000000000..53f5d9bec0 --- /dev/null +++ b/packages/ethereum-contracts/test/foundry/superfluid/PoolAdminNFT.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import { IERC165, IERC721, IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { PoolNFTBaseIntegrationTest, FakePool } from "./PoolNFTBase.t.sol"; +import { IPoolNFTBase } from "../../../contracts/interfaces/agreements/gdav1/IPoolNFTBase.sol"; +import { IPoolAdminNFT } from "../../../contracts/interfaces/agreements/gdav1/IPoolAdminNFT.sol"; +import { ISuperfluidPool } from "../../../contracts/agreements/gdav1/SuperfluidPool.sol"; + +contract PoolAdminNFTIntegrationTest is PoolNFTBaseIntegrationTest { + using Strings for uint256; + + /*////////////////////////////////////////////////////////////////////////// + Revert Tests + //////////////////////////////////////////////////////////////////////////*/ + + function testRevertIfTransferFromForPoolAdminNFT() public { + address poolAdmin = alice; + address receiver = bob; + + ISuperfluidPool pool = sf.gda.createPool(superTokenMock, poolAdmin, poolConfig); + uint256 nftId = _helperGetPoolAdminNftId(address(pool), poolAdmin); + + _helperRevertIfTransferFrom( + poolAdminNFT, poolAdmin, poolAdmin, receiver, nftId, IPoolNFTBase.POOL_NFT_TRANSFER_NOT_ALLOWED.selector + ); + } + + function testRevertIfMintingForFakePool() public { + FakePool pool = new FakePool(alice, address(superTokenMock)); + vm.expectRevert(IPoolNFTBase.POOL_NFT_NOT_REGISTERED_POOL.selector); + poolAdminNFT.mockMint(address(pool)); + } + + function testRevertIfMintingForNotPool(address _pool) public { + vm.expectRevert(); + poolAdminNFT.mockMint(_pool); + } + + /*////////////////////////////////////////////////////////////////////////// + Passing Tests + //////////////////////////////////////////////////////////////////////////*/ + + function testProxiableUUIDIsExpectedValue() public { + assertEq( + poolAdminNFT.proxiableUUID(), keccak256("org.superfluid-finance.contracts.PoolAdminNFT.implementation") + ); + } + + function testTokenURIForPoolAdminNFT(uint256 tokenId) public { + assertEq(poolAdminNFT.tokenURI(tokenId), string(abi.encodePacked(poolAdminNFT.baseURI()))); + } +} diff --git a/packages/ethereum-contracts/test/foundry/superfluid/PoolMemberNFT.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/PoolMemberNFT.t.sol new file mode 100644 index 0000000000..f8feedd2aa --- /dev/null +++ b/packages/ethereum-contracts/test/foundry/superfluid/PoolMemberNFT.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import { IERC165, IERC721, IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { PoolNFTBaseIntegrationTest, FakePool } from "./PoolNFTBase.t.sol"; +import { IPoolNFTBase } from "../../../contracts/interfaces/agreements/gdav1/IPoolNFTBase.sol"; +import { IPoolMemberNFT } from "../../../contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol"; +import { ISuperfluidPool } from "../../../contracts/agreements/gdav1/SuperfluidPool.sol"; +import { IGeneralDistributionAgreementV1 } from "../../../contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; +import "forge-std/Test.sol"; + +contract PoolMemberNFTIntegrationTest is PoolNFTBaseIntegrationTest { + /*////////////////////////////////////////////////////////////////////////// + Revert Tests + //////////////////////////////////////////////////////////////////////////*/ + + function testRevertIfTransferFromForPoolMemberNFT(address _poolAdmin, address _member, address _receiver) public { + vm.assume(_poolAdmin != address(0)); + vm.assume(_member != address(0)); + vm.assume(_receiver != address(0)); + vm.assume(_member != _receiver); + + ISuperfluidPool pool = sf.gda.createPool(superTokenMock, _poolAdmin, poolConfig); + uint256 nftId = _helperGetPoolMemberNftId(address(pool), _member); + + vm.startPrank(_poolAdmin); + pool.updateMemberUnits(_member, 1); + vm.stopPrank(); + + _helperRevertIfTransferFrom( + poolMemberNFT, _member, _member, _receiver, nftId, IPoolNFTBase.POOL_NFT_TRANSFER_NOT_ALLOWED.selector + ); + } + + function testRevertIfMintingForNotPool(address _pool, address _member) public { + vm.expectRevert(); + poolMemberNFT.mockMint(_pool, _member); + } + + function testRevertIfMintingForFakePool(address _admin, address _member) public { + vm.assume(_admin != address(0)); + vm.assume(_member != address(0)); + FakePool pool = new FakePool(_admin, address(superTokenMock)); + vm.expectRevert(IPoolNFTBase.POOL_NFT_NOT_REGISTERED_POOL.selector); + poolMemberNFT.mockMint(address(pool), _member); + } + + function testRevertIfMintingForZeroUnitMember() public { + address admin_ = alice; + address member = bob; + ISuperfluidPool pool = sf.gda.createPool(superTokenMock, admin_, poolConfig); + vm.expectRevert(IPoolMemberNFT.POOL_MEMBER_NFT_NO_UNITS.selector); + poolMemberNFT.mockMint(address(pool), member); + } + + function testRevertIfBurningNFTOfMemberWithUnits(address _admin, address _member) public { + vm.assume(_admin != address(0)); + vm.assume(_member != address(0)); + ISuperfluidPool pool = sf.gda.createPool(superTokenMock, _admin, poolConfig); + uint256 nftId = _helperGetPoolMemberNftId(address(pool), _member); + + vm.startPrank(_admin); + pool.updateMemberUnits(_member, 1); + vm.stopPrank(); + + vm.expectRevert(IPoolMemberNFT.POOL_MEMBER_NFT_HAS_UNITS.selector); + poolMemberNFT.mockBurn(nftId); + } + + /*////////////////////////////////////////////////////////////////////////// + Passing Tests + //////////////////////////////////////////////////////////////////////////*/ + + function testProxiableUUIDIsExpectedValue() public { + assertEq( + poolMemberNFT.proxiableUUID(), keccak256("org.superfluid-finance.contracts.PoolMemberNFT.implementation") + ); + } + + function testTokenURIForPoolMemberNFT(uint256 tokenId) public { + assertEq(poolMemberNFT.tokenURI(tokenId), string(abi.encodePacked(poolMemberNFT.baseURI()))); + } +} diff --git a/packages/ethereum-contracts/test/foundry/superfluid/PoolNFTBase.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/PoolNFTBase.t.sol new file mode 100644 index 0000000000..8d3a7e43d4 --- /dev/null +++ b/packages/ethereum-contracts/test/foundry/superfluid/PoolNFTBase.t.sol @@ -0,0 +1,408 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import { IERC165, IERC721, IERC721Metadata } from "@openzeppelin/contracts/interfaces/IERC721Metadata.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { SuperTokenV1Library } from "../../../contracts/apps/SuperTokenV1Library.sol"; +import { + PoolNFTBaseStorageLayoutMock, + PoolAdminNFTStorageLayoutMock, + PoolMemberNFTStorageLayoutMock +} from "../../../contracts/mocks/PoolNFTUpgradabilityMock.sol"; +import { IPoolNFTBase, PoolNFTBase } from "../../../contracts/agreements/gdav1/PoolNFTBase.sol"; +import { ConstantOutflowNFT, IConstantOutflowNFT } from "../../../contracts/superfluid/ConstantOutflowNFT.sol"; +import { ConstantInflowNFT, IConstantInflowNFT } from "../../../contracts/superfluid/ConstantInflowNFT.sol"; +import { TestToken } from "../../../contracts/utils/TestToken.sol"; +import { PoolAdminNFT, IPoolAdminNFT } from "../../../contracts/agreements/gdav1/PoolAdminNFT.sol"; +import { PoolMemberNFT, IPoolMemberNFT } from "../../../contracts/agreements/gdav1/PoolMemberNFT.sol"; +import { ConstantOutflowNFTMock, ConstantInflowNFTMock } from "../../../contracts/mocks/CFAv1NFTMock.sol"; +import { PoolNFTBaseMock } from "../../../contracts/mocks/PoolNFTMock.sol"; +import { ISuperfluidPool } from "../../../contracts/agreements/gdav1/SuperfluidPool.sol"; +import { ERC721IntegrationTest } from "./ERC721.t.sol"; + +/// @title PoolNFTBaseIntegrationTest +/// @author Superfluid +/// @dev This is a base contract for testing PoolNFTBase +/// We test the functions in the PoolNFTBase directly via the base contract +/// and the assumption is that because it is tested here, it is tested for all +/// the derived contracts. +abstract contract PoolNFTBaseIntegrationTest is ERC721IntegrationTest { + using Strings for uint256; + + string public constant NAME = "Pool NFT Base"; + string public constant SYMBOL = "PNFTB"; + + PoolNFTBaseMock public poolNFTBaseMock; + + function setUp() public virtual override { + super.setUp(); + poolNFTBaseMock = new PoolNFTBaseMock(sf.host); + poolNFTBaseMock.initialize(NAME, SYMBOL); + } + + /*////////////////////////////////////////////////////////////////////////// + Revert Tests + //////////////////////////////////////////////////////////////////////////*/ + function testRevertIfContractAlreadyInitialized() public { + vm.expectRevert("Initializable: contract is already initialized"); + + poolNFTBaseMock.initialize(NAME, SYMBOL); + } + + function testRevertIfOwnerOfForNonExistentToken(uint256 _tokenId) public { + _helperRevertIfOwnerOf(poolAdminNFT, _tokenId, IPoolNFTBase.POOL_NFT_INVALID_TOKEN_ID.selector); + } + + function testRevertIfGetApprovedCalledForNonExistentToken(uint256 _tokenId) public { + _helperRevertIfGetApproved(poolAdminNFT, _tokenId, IPoolNFTBase.POOL_NFT_INVALID_TOKEN_ID.selector); + } + + function testRevertIfSetApprovalForAllOperatorApproveToCaller(address _account) public { + vm.assume(_account != address(0)); + + vm.startPrank(_account); + vm.expectRevert(IPoolNFTBase.POOL_NFT_APPROVE_TO_CALLER.selector); + poolNFTBaseMock.setApprovalForAll(_account, true); + vm.stopPrank(); + } + + function testRevertIfApproveToCurrentOwner(address _pool, address _account) public { + vm.assume(_pool != address(0)); + vm.assume(_account != address(0)); + + uint256 nftId = _helperGetPoolNFTBaseMockNftId(_pool, _account); + poolNFTBaseMock.mockMint(_pool, _account); + + vm.startPrank(_account); + vm.expectRevert(IPoolNFTBase.POOL_NFT_APPROVE_TO_CURRENT_OWNER.selector); + poolNFTBaseMock.approve(_account, nftId); + vm.stopPrank(); + } + + function testRevertIfApproveAsNonOwner(address _pool, address _account, address _approver, address _approvedAccount) + public + { + vm.assume(_pool != address(0)); + vm.assume(_account != address(0)); + /// @dev _account is owner of pool NFT + vm.assume(_approver != _account); + vm.assume(_approvedAccount != _account); + + uint256 nftId = _helperGetPoolNFTBaseMockNftId(_pool, _account); + poolNFTBaseMock.mockMint(_pool, _account); + vm.expectRevert(IPoolNFTBase.POOL_NFT_APPROVE_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector); + vm.startPrank(_approver); + poolNFTBaseMock.approve(_approvedAccount, nftId); + vm.stopPrank(); + } + + function testRevertIfTransferFrom(address _pool, address _account, address _recipient) public { + vm.assume(_pool != address(0)); + vm.assume(_account != address(0)); + vm.assume(_recipient != address(0)); + + uint256 nftId = _helperGetPoolNFTBaseMockNftId(_pool, _account); + + poolNFTBaseMock.mockMint(address(_pool), _account); + + _helperRevertIfTransferFrom( + poolNFTBaseMock, _account, _account, _recipient, nftId, IPoolNFTBase.POOL_NFT_TRANSFER_NOT_ALLOWED.selector + ); + } + + function testRevertIfSafeTransferFrom(address _pool, address _account, address _recipient) public { + vm.assume(_pool != address(0)); + vm.assume(_account != address(0)); + vm.assume(_recipient != address(0)); + + uint256 nftId = _helperGetPoolNFTBaseMockNftId(_pool, _account); + + poolNFTBaseMock.mockMint(address(_pool), _account); + + _helperRevertIfSafeTransferFrom( + poolNFTBaseMock, _account, _account, _recipient, nftId, IPoolNFTBase.POOL_NFT_TRANSFER_NOT_ALLOWED.selector + ); + } + + function testRevertIfSafeTransferFromWithData(address _pool, address _account, address _recipient) public { + vm.assume(_pool != address(0)); + vm.assume(_account != address(0)); + vm.assume(_recipient != address(0)); + + uint256 nftId = _helperGetPoolNFTBaseMockNftId(_pool, _account); + + poolNFTBaseMock.mockMint(address(_pool), _account); + + _helperRevertIfSafeTransferFrom( + poolNFTBaseMock, + _account, + _account, + _recipient, + nftId, + "0x", + IPoolNFTBase.POOL_NFT_TRANSFER_NOT_ALLOWED.selector + ); + } + + function testRevertIfTransferFromAsNonOwner(address _pool, address _account, address _recipient) public { + vm.assume(_pool != address(0)); + vm.assume(_account != address(0)); + vm.assume(_recipient != address(0)); + vm.assume(_recipient != _account); + + uint256 nftId = _helperGetPoolNFTBaseMockNftId(_pool, _account); + + poolNFTBaseMock.mockMint(address(_pool), _account); + + _helperRevertIfTransferFrom( + poolNFTBaseMock, + _recipient, + _account, + _recipient, + nftId, + IPoolNFTBase.POOL_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector + ); + } + + function testRevertIfSafeTransferFromAsNonOwner(address _pool, address _account, address _recipient) public { + vm.assume(_pool != address(0)); + vm.assume(_account != address(0)); + vm.assume(_recipient != address(0)); + vm.assume(_recipient != _account); + + uint256 nftId = _helperGetPoolNFTBaseMockNftId(_pool, _account); + + poolNFTBaseMock.mockMint(address(_pool), _account); + + _helperRevertIfSafeTransferFrom( + poolNFTBaseMock, + _recipient, + _account, + _recipient, + nftId, + IPoolNFTBase.POOL_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector + ); + } + + function testRevertIfSafeTransferFromWithDataAsNonOwner(address _pool, address _account, address _recipient) + public + { + vm.assume(_pool != address(0)); + vm.assume(_account != address(0)); + vm.assume(_recipient != address(0)); + vm.assume(_recipient != _account); + + uint256 nftId = _helperGetPoolNFTBaseMockNftId(_pool, _account); + + poolNFTBaseMock.mockMint(address(_pool), _account); + + _helperRevertIfSafeTransferFrom( + poolNFTBaseMock, + _recipient, + _account, + _recipient, + nftId, + "0x", + IPoolNFTBase.POOL_NFT_TRANSFER_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL.selector + ); + } + + /*////////////////////////////////////////////////////////////////////////// + Passing Tests + //////////////////////////////////////////////////////////////////////////*/ + + function testContractSupportsExpectedInterfaces() public { + assertEq(poolNFTBaseMock.supportsInterface(type(IERC165).interfaceId), true); + assertEq(poolNFTBaseMock.supportsInterface(type(IERC721).interfaceId), true); + assertEq(poolNFTBaseMock.supportsInterface(type(IERC721Metadata).interfaceId), true); + } + + function testBalanceOfIsAlwaysOne(address owner) public { + assertEq(poolNFTBaseMock.balanceOf(owner), 1, "PoolNFTBase: balanceOf is not always one"); + } + + function testHostIsProperlySetInConstructor() public { + assertEq(address(poolNFTBaseMock.HOST()), address(sf.host)); + } + + function testGDAv1IsProperlySetInConstructor() public { + assertEq(address(poolNFTBaseMock.GENERAL_DISTRIBUTION_AGREEMENT_V1()), address(sf.gda)); + } + + function testNFTMetadataIsProperlyInitialized() public { + assertEq(poolNFTBaseMock.name(), NAME); + assertEq(poolNFTBaseMock.symbol(), SYMBOL); + } + + function testTokenURI(uint256 tokenId) public { + assertEq(poolNFTBaseMock.tokenURI(tokenId), string(abi.encodePacked("tokenId=", tokenId.toString()))); + } + + function testTriggerMetadataUpdate(uint256 tokenId) public { + _assertEventMetadataUpdate(address(poolNFTBaseMock), tokenId); + poolNFTBaseMock.triggerMetadataUpdate(tokenId); + } + + function testApprove(address _account, address _pool, address _approvedAccount) + public + virtual + returns (uint256 nftId) + { + vm.assume(_account != address(0)); + vm.assume(_pool != address(0)); + vm.assume(_account != _approvedAccount); + + nftId = _helperGetPoolNFTBaseMockNftId(_pool, _account); + poolNFTBaseMock.mockMint(_pool, _account); + + _assertEventApproval(address(poolNFTBaseMock), _account, _approvedAccount, nftId); + + vm.startPrank(_account); + poolNFTBaseMock.approve(_approvedAccount, nftId); + vm.stopPrank(); + + _assertApprovalIsExpected(poolNFTBaseMock, nftId, _approvedAccount); + } + + function testSetApprovalForAll(address _tokenOwner, address _operator, bool _approved) public { + vm.assume(_tokenOwner != address(0)); + vm.assume(_tokenOwner != _operator); + + _assertEventApprovalForAll(address(poolNFTBaseMock), _tokenOwner, _operator, _approved); + + vm.startPrank(_tokenOwner); + poolNFTBaseMock.setApprovalForAll(_operator, _approved); + vm.stopPrank(); + + _assertOperatorApprovalIsExpected(poolNFTBaseMock, _tokenOwner, _operator, _approved); + } + + /*////////////////////////////////////////////////////////////////////////// + Helper Functions + //////////////////////////////////////////////////////////////////////////*/ + function _helperGetPoolNFTBaseMockNftId(address _pool, address _account) internal view returns (uint256) { + return poolNFTBaseMock.getTokenId(_pool, _account); + } + + function _helperGetPoolAdminNftId(address _pool, address _poolAdmin) internal view returns (uint256) { + return poolAdminNFT.getTokenId(_pool, _poolAdmin); + } + + function _helperGetPoolMemberNftId(address _pool, address _poolMember) internal view returns (uint256) { + return poolMemberNFT.getTokenId(_pool, _poolMember); + } + + /*////////////////////////////////////////////////////////////////////////// + Assertion Helpers + //////////////////////////////////////////////////////////////////////////*/ + function _assertPoolAdminNftStateIsExpected(uint256 _tokenId, address _expectedPool, address _expectedAdmin) + public + { + PoolAdminNFT.PoolAdminNFTData memory poolAdminNFTData = poolAdminNFT.poolAdminDataByTokenId(_tokenId); + + assertEq(poolAdminNFTData.pool, _expectedPool, "PoolAdminNFT: pool address not as expected"); + + // assert admin is equal to expected admin + assertEq(poolAdminNFTData.admin, _expectedAdmin, "PoolAdminNFT: admin address not as expected"); + + // assert owner of pool admin nft equal to expected admin + _assertOwnerOfIsExpected( + poolAdminNFT, _tokenId, _expectedAdmin, "PoolAdminNFT: owner of pool admin nft not as expected" + ); + } + + function _assertPoolMemberNftStateIsExpected( + uint256 _tokenId, + address _expectedPool, + address _expectedMember, + uint128 _expectedUnits + ) public { + PoolMemberNFT.PoolMemberNFTData memory poolMemberNFTData = poolMemberNFT.poolMemberDataByTokenId(_tokenId); + + assertEq(poolMemberNFTData.pool, _expectedPool, "PoolMemberNFT: pool address not as expected"); + + // assert member is equal to expected member + assertEq(poolMemberNFTData.member, _expectedMember, "PoolMemberNFT: member address not as expected"); + + // assert units is equal to expected units + assertEq(poolMemberNFTData.units, _expectedUnits, "PoolMemberNFT: units not as expected"); + + // assert owner of pool member nft equal to expected member + _assertOwnerOfIsExpected( + poolAdminNFT, _tokenId, _expectedMember, "PoolMemberNFT: owner of pool member nft not as expected" + ); + } +} + +/// @title PoolNFTUpgradabilityTest +/// @author Superfluid +/// @notice Used for testing storage layout and upgradability of Pool NFT contracts +contract PoolNFTUpgradabilityTest is PoolNFTBaseIntegrationTest { + /*////////////////////////////////////////////////////////////////////////// + Storage Layout Tests + //////////////////////////////////////////////////////////////////////////*/ + function testPoolNFTBaseStorageLayout() public { + PoolNFTBaseStorageLayoutMock poolNFTBaseStorageLayoutMock = new PoolNFTBaseStorageLayoutMock(sf.host); + + poolNFTBaseStorageLayoutMock.validateStorageLayout(); + } + + function testPoolMemberNFTStorageLayout() public { + PoolMemberNFTStorageLayoutMock poolMemberNFTStorageLayoutMock = new PoolMemberNFTStorageLayoutMock(sf.host); + + poolMemberNFTStorageLayoutMock.validateStorageLayout(); + } + + function testPoolAdminNFTStorageLayout() public { + PoolAdminNFTStorageLayoutMock poolAdminNFTStorageLayoutMock = new PoolAdminNFTStorageLayoutMock(sf.host); + + poolAdminNFTStorageLayoutMock.validateStorageLayout(); + } + + /*////////////////////////////////////////////////////////////////////////// + Revert Tests + //////////////////////////////////////////////////////////////////////////*/ + function testRevertPoolNFTContractsCannotBeUpgradedByNonSuperTokenFactory(address notSuperTokenFactory) public { + vm.assume(notSuperTokenFactory != address(sf.superTokenFactory)); + PoolAdminNFT newPoolAdminNFT = new PoolAdminNFT( + sf.host + ); + vm.expectRevert(IPoolNFTBase.POOL_NFT_ONLY_SUPER_TOKEN_FACTORY.selector); + vm.prank(notSuperTokenFactory); + poolAdminNFT.updateCode(address(newPoolAdminNFT)); + + PoolMemberNFT newPoolMemberNFT = new PoolMemberNFT( + sf.host + ); + vm.expectRevert(IPoolNFTBase.POOL_NFT_ONLY_SUPER_TOKEN_FACTORY.selector); + vm.prank(notSuperTokenFactory); + poolMemberNFT.updateCode(address(newPoolMemberNFT)); + } + + /*////////////////////////////////////////////////////////////////////////// + Passing Tests + //////////////////////////////////////////////////////////////////////////*/ + function testPoolNFTContractsCanBeUpgradedBySuperTokenFactory() public { + PoolAdminNFT newPoolAdminNFT = new PoolAdminNFT( + sf.host + ); + vm.prank(address(sf.superTokenFactory)); + poolAdminNFT.updateCode(address(newPoolAdminNFT)); + + PoolMemberNFT newPoolMemberNFT = new PoolMemberNFT( + sf.host + ); + vm.prank(address(sf.superTokenFactory)); + poolMemberNFT.updateCode(address(newPoolMemberNFT)); + } +} + +contract FakePool { + address public admin; + address public superToken; + + constructor(address _admin, address _superToken) { + admin = _admin; + superToken = _superToken; + } +} diff --git a/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol index 9639a11401..bbd2d120e4 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol @@ -7,6 +7,8 @@ import { UUPSProxiable } from "../../../contracts/upgradability/UUPSProxiable.so import { IERC20, ISuperToken, SuperToken } from "../../../contracts/superfluid/SuperToken.sol"; import { ConstantOutflowNFT, IConstantOutflowNFT } from "../../../contracts/superfluid/ConstantOutflowNFT.sol"; import { ConstantInflowNFT, IConstantInflowNFT } from "../../../contracts/superfluid/ConstantInflowNFT.sol"; +import { PoolAdminNFT, IPoolAdminNFT } from "../../../contracts/agreements/gdav1/PoolAdminNFT.sol"; +import { PoolMemberNFT, IPoolMemberNFT } from "../../../contracts/agreements/gdav1/PoolMemberNFT.sol"; import { FoundrySuperfluidTester } from "../FoundrySuperfluidTester.sol"; import { TestToken } from "../../../contracts/utils/TestToken.sol"; import { TokenDeployerLibrary } from "../../../contracts/utils/SuperfluidFrameworkDeploymentSteps.sol"; @@ -27,7 +29,7 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester { // We assume that most underlying tokens will not have more than 32 decimals vm.assume(decimals <= 32); (TestToken localToken, ISuperToken localSuperToken) = - sfDeployer.deployWrapperSuperToken("FTT", "FTT", decimals, type(uint256).max); + sfDeployer.deployWrapperSuperToken("FTT", "FTT", decimals, type(uint256).max, address(0)); (uint256 underlyingAmount, uint256 adjustedAmount) = localSuperToken.toUnderlyingAmount(amount); localToken.mint(alice, INIT_TOKEN_BALANCE); vm.startPrank(alice); @@ -46,7 +48,7 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester { vm.assume(decimals <= 32); vm.assume(downgradeAmount < upgradeAmount); (TestToken localToken, ISuperToken localSuperToken) = - sfDeployer.deployWrapperSuperToken("FTT", "FTT", decimals, type(uint256).max); + sfDeployer.deployWrapperSuperToken("FTT", "FTT", decimals, type(uint256).max, address(0)); (uint256 underlyingAmount, uint256 adjustedAmount) = localSuperToken.toUnderlyingAmount(upgradeAmount); localToken.mint(alice, INIT_TOKEN_BALANCE); @@ -70,6 +72,8 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester { function testRevertSuperTokenUpdateCodeWrongNFTProxies() public { UUPSProxy cifProxy = new UUPSProxy(); UUPSProxy cofProxy = new UUPSProxy(); + UUPSProxy paProxy = new UUPSProxy(); + UUPSProxy pmProxy = new UUPSProxy(); ConstantInflowNFT cifNFTLogic = new ConstantInflowNFT( sf.host, @@ -79,21 +83,35 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester { sf.host, IConstantInflowNFT(address(cifProxy)) ); + PoolAdminNFT paNFTLogic = new PoolAdminNFT( + sf.host + ); + PoolMemberNFT pmNFTLogic = new PoolMemberNFT( + sf.host + ); cifNFTLogic.castrate(); cofNFTLogic.castrate(); + paNFTLogic.castrate(); + pmNFTLogic.castrate(); cifProxy.initializeProxy(address(cifNFTLogic)); cofProxy.initializeProxy(address(cofNFTLogic)); + paProxy.initializeProxy(address(paNFTLogic)); + pmProxy.initializeProxy(address(pmNFTLogic)); ConstantInflowNFT(address(cofProxy)).initialize("Constant Outflow NFT", "COF"); ConstantOutflowNFT(address(cifProxy)).initialize("Constant Inflow NFT", "CIF"); + PoolAdminNFT(address(paProxy)).initialize("Pool Admin NFT", "PA"); + PoolMemberNFT(address(pmProxy)).initialize("Pool Member NFT", "PM"); - // both nft proxies incorrect + // all nft proxies incorrect SuperToken superTokenLogic = new SuperToken( sf.host, ConstantOutflowNFT(address(cofProxy)), - ConstantInflowNFT(address(cifProxy)) + ConstantInflowNFT(address(cifProxy)), + PoolAdminNFT(address(paProxy)), + PoolMemberNFT(address(pmProxy)) ); vm.prank(address(sf.host)); vm.expectRevert(ISuperToken.SUPER_TOKEN_NFT_PROXY_ADDRESS_CHANGED.selector); @@ -103,7 +121,9 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester { superTokenLogic = new SuperToken( sf.host, superToken.CONSTANT_OUTFLOW_NFT(), - ConstantInflowNFT(address(cifProxy)) + ConstantInflowNFT(address(cifProxy)), + superToken.POOL_ADMIN_NFT(), + superToken.POOL_MEMBER_NFT() ); vm.prank(address(sf.host)); vm.expectRevert(ISuperToken.SUPER_TOKEN_NFT_PROXY_ADDRESS_CHANGED.selector); @@ -113,7 +133,9 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester { superTokenLogic = new SuperToken( sf.host, ConstantOutflowNFT(address(cofProxy)), - superToken.CONSTANT_INFLOW_NFT() + superToken.CONSTANT_INFLOW_NFT(), + superToken.POOL_ADMIN_NFT(), + superToken.POOL_MEMBER_NFT() ); vm.prank(address(sf.host)); vm.expectRevert(ISuperToken.SUPER_TOKEN_NFT_PROXY_ADDRESS_CHANGED.selector); @@ -132,7 +154,8 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester { } function testOnlyHostCanChangeAdminWhenNoAdmin(address _admin) public { - (, ISuperToken localSuperToken) = sfDeployer.deployWrapperSuperToken("FTT", "FTT", 18, type(uint256).max); + (, ISuperToken localSuperToken) = + sfDeployer.deployWrapperSuperToken("FTT", "FTT", 18, type(uint256).max, address(0)); vm.startPrank(address(sf.host)); localSuperToken.changeAdmin(_admin); @@ -180,9 +203,25 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester { vm.stopPrank(); } + function testRevertWhenNonAdminTriesToUpdateCode(address _admin, address nonAdmin) public { + vm.assume(_admin != address(sf.host)); + vm.assume(nonAdmin != address(sf.host)); + + (TestToken localTestToken, ISuperToken localSuperToken) = + sfDeployer.deployWrapperSuperToken("FTT", "FTT", 18, type(uint256).max, address(0)); + + SuperToken newSuperTokenLogic = + _helperDeploySuperTokenAndInitialize(localSuperToken, localTestToken, 18, "FTT", "FTT", _admin); + + vm.startPrank(nonAdmin); + vm.expectRevert(ISuperToken.SUPER_TOKEN_ONLY_ADMIN.selector); + UUPSProxiable(address(localSuperToken)).updateCode(address(newSuperTokenLogic)); + vm.stopPrank(); + } + function testOnlyHostCanUpdateCodeWhenNoAdmin() public { (TestToken localTestToken, ISuperToken localSuperToken) = - sfDeployer.deployWrapperSuperToken("FTT", "FTT", 18, type(uint256).max); + sfDeployer.deployWrapperSuperToken("FTT", "FTT", 18, type(uint256).max, address(0)); SuperToken newSuperTokenLogic = _helperDeploySuperTokenAndInitialize(localSuperToken, localTestToken, 18, "FTT", "FTT", address(0)); @@ -219,19 +258,4 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester { "testOnlyHostCanUpdateCodeWhenNoAdmin: super token logic not updated correctly" ); } - - function testRevertWhenNonAdminTriesToUpdateCode(address _admin, address nonAdmin) public { - vm.assume(_admin != address(sf.host)); - - (TestToken localTestToken, ISuperToken localSuperToken) = - sfDeployer.deployWrapperSuperToken("FTT", "FTT", 18, type(uint256).max); - - SuperToken newSuperTokenLogic = - _helperDeploySuperTokenAndInitialize(localSuperToken, localTestToken, 18, "FTT", "FTT", _admin); - - vm.startPrank(nonAdmin); - vm.expectRevert(ISuperToken.SUPER_TOKEN_ONLY_ADMIN.selector); - UUPSProxiable(address(localSuperToken)).updateCode(address(newSuperTokenLogic)); - vm.stopPrank(); - } } diff --git a/packages/ethereum-contracts/test/foundry/superfluid/SuperTokenFactory.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/SuperTokenFactory.t.sol index f7ac867161..90c0c991af 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/SuperTokenFactory.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/SuperTokenFactory.t.sol @@ -5,6 +5,8 @@ import { FoundrySuperfluidTester } from "../FoundrySuperfluidTester.sol"; import { SuperTokenFactory } from "../../../contracts/superfluid/SuperTokenFactory.sol"; import { ConstantOutflowNFT, IConstantOutflowNFT } from "../../../contracts/superfluid/ConstantOutflowNFT.sol"; import { ConstantInflowNFT, IConstantInflowNFT } from "../../../contracts/superfluid/ConstantInflowNFT.sol"; +import { PoolAdminNFT, IPoolAdminNFT } from "../../../contracts/agreements/gdav1/PoolAdminNFT.sol"; +import { PoolMemberNFT, IPoolMemberNFT } from "../../../contracts/agreements/gdav1/PoolMemberNFT.sol"; import { ISuperToken, SuperToken } from "../../../contracts/superfluid/SuperToken.sol"; import { UUPSProxiable } from "../../../contracts/upgradability/UUPSProxiable.sol"; @@ -19,7 +21,9 @@ contract SuperTokenFactoryTest is FoundrySuperfluidTester { SuperToken newSuperTokenLogic = new SuperToken( sf.host, superToken.CONSTANT_OUTFLOW_NFT(), - superToken.CONSTANT_INFLOW_NFT() + superToken.CONSTANT_INFLOW_NFT(), + superToken.POOL_ADMIN_NFT(), + superToken.POOL_MEMBER_NFT() ); ConstantOutflowNFT newConstantOutflowNFTLogic = new ConstantOutflowNFT( sf.host, @@ -29,6 +33,8 @@ contract SuperTokenFactoryTest is FoundrySuperfluidTester { sf.host, IConstantOutflowNFT(address(superToken.CONSTANT_OUTFLOW_NFT())) ); + PoolAdminNFT newPoolAdminNFTLogic = new PoolAdminNFT(sf.host); + PoolMemberNFT newPoolMemberNFTLogic = new PoolMemberNFT(sf.host); assertEq( UUPSProxiable(address(superToken.CONSTANT_OUTFLOW_NFT())).getCodeAddress(), address(sf.superTokenFactory.CONSTANT_OUTFLOW_NFT_LOGIC()) @@ -41,7 +47,9 @@ contract SuperTokenFactoryTest is FoundrySuperfluidTester { sf.host, newSuperTokenLogic, newConstantOutflowNFTLogic, - newConstantInflowNFTLogic + newConstantInflowNFTLogic, + newPoolAdminNFTLogic, + newPoolMemberNFTLogic ); vm.startPrank(address(sf.host)); // We expect this to revert if the protocol is not upgradeable diff --git a/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol index ff5d9e51c4..ffd5cd1163 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol @@ -12,7 +12,7 @@ import { AgreementMock } from "../../../contracts/mocks/AgreementMock.sol"; contract SuperfluidIntegrationTest is FoundrySuperfluidTester { using SuperTokenV1Library for SuperToken; - uint32 private constant _NUM_AGREEMENTS = 2; + uint32 private constant _NUM_AGREEMENTS = 3; constructor() FoundrySuperfluidTester(3) { } @@ -23,15 +23,16 @@ contract SuperfluidIntegrationTest is FoundrySuperfluidTester { ); mocks[0] = ISuperAgreement(address(sf.cfa)); mocks[1] = ISuperAgreement(address(sf.ida)); + mocks[2] = ISuperAgreement(address(sf.gda)); for (uint256 i; i < maxNumAgreements - _NUM_AGREEMENTS; ++i) { bytes32 id = keccak256(abi.encode("type.", i)); - AgreementMock mock = new AgreementMock(address(sf.host), id, i); + AgreementMock agreementMock = new AgreementMock(address(sf.host), id, i); vm.startPrank(sf.governance.owner()); - sf.governance.registerAgreementClass(sf.host, address(mock)); + sf.governance.registerAgreementClass(sf.host, address(agreementMock)); vm.stopPrank(); - mock = sf.host.NON_UPGRADABLE_DEPLOYMENT() ? mock : AgreementMock(address(sf.host.getAgreementClass(id))); - mocks[i + _NUM_AGREEMENTS] = ISuperAgreement(address(mock)); + agreementMock = sf.host.NON_UPGRADABLE_DEPLOYMENT() ? agreementMock : AgreementMock(address(sf.host.getAgreementClass(id))); + mocks[i + _NUM_AGREEMENTS] = ISuperAgreement(address(agreementMock)); } ISuperAgreement[] memory agreementClasses = sf.host.mapAgreementClasses(type(uint256).max); diff --git a/packages/ethereum-contracts/test/foundry/superfluid/SuperfluidPool.prop.sol b/packages/ethereum-contracts/test/foundry/superfluid/SuperfluidPool.prop.sol new file mode 100644 index 0000000000..1e9f68018f --- /dev/null +++ b/packages/ethereum-contracts/test/foundry/superfluid/SuperfluidPool.prop.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import "@superfluid-finance/solidity-semantic-money/src/SemanticMoney.sol"; +import { GeneralDistributionAgreementV1 } from "../../../contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol"; +import { SuperfluidPool } from "../../../contracts/agreements/gdav1/SuperfluidPool.sol"; + +/// @title SuperfluidPool Property Tests +/// @author Superfluid +/// @notice This is a contract that runs property tests for the SuperfluidPool +/// It involves testing the pure functions of the SuperfluidPool to ensure that we get +/// the expected output for a range of inputs. +contract SuperfluidPoolProperties is SuperfluidPool, Test { + constructor() SuperfluidPool(GeneralDistributionAgreementV1(address(0))) { } + + function _helperAssertWrappedParticle(PoolIndexData memory poolIndexData, BasicParticle memory particle) internal { + assertEq( + FlowRate.unwrap(particle.flow_rate()), + int128(poolIndexData.wrappedFlowRate), + "SuperfluidPool.prop (PoolIndex): flowRate not equal" + ); + assertEq( + Time.unwrap(particle.settled_at()), + poolIndexData.wrappedSettledAt, + "SuperfluidPool.prop (PoolIndex): settledAt not equal" + ); + assertEq( + Value.unwrap(particle._settled_value), + poolIndexData.wrappedSettledValue, + "SuperfluidPool.prop (PoolIndex): settledValue not equal" + ); + } + + function _helperAssertWrappedParticle(MemberData memory memberData, BasicParticle memory particle) internal { + assertEq( + FlowRate.unwrap(particle.flow_rate()), + int128(memberData.syncedFlowRate), + "SuperfluidPool.prop (BasicParticle): flowRate not equal" + ); + assertEq( + Time.unwrap(particle.settled_at()), + memberData.syncedSettledAt, + "SuperfluidPool.prop (BasicParticle): settledAt not equal" + ); + assertEq( + Value.unwrap(particle._settled_value), + memberData.syncedSettledValue, + "SuperfluidPool.prop (BasicParticle): settledValue not equal" + ); + } + + function testPoolIndexDataToWrappedParticle(PoolIndexData memory data) public { + BasicParticle memory wrappedParticle = _poolIndexDataToWrappedParticle(data); + _helperAssertWrappedParticle(data, wrappedParticle); + } + + function testPoolIndexDataToPDPoolIndex(PoolIndexData memory data) public { + vm.assume(data.totalUnits < uint128(type(int128).max)); + + PDPoolIndex memory pdPoolIndex = poolIndexDataToPDPoolIndex(data); + assertEq( + uint128(Unit.unwrap(pdPoolIndex.total_units)), data.totalUnits, "SuperfluidPool.prop: total units not equal" + ); + _helperAssertWrappedParticle(data, pdPoolIndex._wrapped_particle); + } + + function testPDPoolIndexToPoolIndexData( + int128 totalUnits, + uint32 wrappedSettledAt, + int96 wrappedFlowRate, + int256 wrappedSettledValue + ) public { + vm.assume(totalUnits > 0); + PDPoolIndex memory pdPoolIndex = PDPoolIndex( + Unit.wrap(totalUnits), + BasicParticle(Time.wrap(wrappedSettledAt), FlowRate.wrap(wrappedFlowRate), Value.wrap(wrappedSettledValue)) + ); + PoolIndexData memory poolIndexData = _pdPoolIndexToPoolIndexData(pdPoolIndex); + assertEq( + poolIndexData.totalUnits, + uint128(Unit.unwrap(pdPoolIndex.total_units)), + "SuperfluidPool.prop: total units not equal" + ); + _helperAssertWrappedParticle(poolIndexData, pdPoolIndex._wrapped_particle); + } + + function testMemberDataToPDPoolMember(MemberData memory data) public { + vm.assume(data.ownedUnits < uint128(type(int128).max)); + + PDPoolMember memory pdPoolMember = _memberDataToPDPoolMember(data); + assertEq( + uint128(Unit.unwrap(pdPoolMember.owned_units)), + data.ownedUnits, + "SuperfluidPool.prop: owned units not equal" + ); + assertEq( + Value.unwrap(pdPoolMember._settled_value), data.settledValue, "SuperfluidPool.prop: settled value not equal" + ); + _helperAssertWrappedParticle(data, pdPoolMember._synced_particle); + } +} diff --git a/packages/ethereum-contracts/test/foundry/upgradability/SuperfluidUpgradeableBeacon.t.sol b/packages/ethereum-contracts/test/foundry/upgradability/SuperfluidUpgradeableBeacon.t.sol new file mode 100644 index 0000000000..c505fc7522 --- /dev/null +++ b/packages/ethereum-contracts/test/foundry/upgradability/SuperfluidUpgradeableBeacon.t.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.19; + +import { Test } from "forge-std/Test.sol"; + +import { SuperfluidUpgradeableBeacon } from "../../../contracts/upgradability/SuperfluidUpgradeableBeacon.sol"; + +import { BeaconProxiable } from "../../../contracts/upgradability/BeaconProxiable.sol"; + +contract ProxiableBeacon is BeaconProxiable { + function proxiableUUID() public pure override returns (bytes32) { + return keccak256("ProxiableBeacon"); + } +} + +contract BadProxiableBeacon is BeaconProxiable { + function proxiableUUID() public pure override returns (bytes32) { + return keccak256("BadProxiableBeacon"); + } +} + +contract SuperfluidUpgradeableBeaconTest is Test { + address public constant owner = address(0x420); + SuperfluidUpgradeableBeacon public beacon; + + function setUp() public { + vm.startPrank(owner); + ProxiableBeacon proxiableBeacon = new ProxiableBeacon(); + beacon = new SuperfluidUpgradeableBeacon(address(proxiableBeacon)); + vm.stopPrank(); + } + + function testRevertNonOwnerUpgrade() public { + ProxiableBeacon proxiableBeacon = new ProxiableBeacon(); + vm.expectRevert("Ownable: caller is not the owner"); + beacon.upgradeTo(address(proxiableBeacon)); + } + + function testRevertUpgradeToZeroAddress() public { + vm.expectRevert(SuperfluidUpgradeableBeacon.ZERO_ADDRESS_IMPLEMENTATION.selector); + vm.startPrank(owner); + beacon.upgradeTo(address(0)); + vm.stopPrank(); + } + + function testRevertUpgradeToIncompatibleLogic() public { + BadProxiableBeacon badProxiableBeacon = new BadProxiableBeacon(); + vm.expectRevert(SuperfluidUpgradeableBeacon.INCOMPATIBLE_LOGIC.selector); + vm.startPrank(owner); + beacon.upgradeTo(address(badProxiableBeacon)); + vm.stopPrank(); + } + + function testRevertWhenDoingProxyLoop() public { + vm.expectRevert(SuperfluidUpgradeableBeacon.NO_PROXY_LOOP.selector); + vm.startPrank(owner); + beacon.upgradeTo(address(beacon)); + vm.stopPrank(); + } + + function testUpgradeTo() public { + ProxiableBeacon proxiableBeacon = new ProxiableBeacon(); + vm.startPrank(owner); + beacon.upgradeTo(address(proxiableBeacon)); + vm.stopPrank(); + assertEq( + beacon.implementation(), address(proxiableBeacon), "SuperfluidUpgradeableBeacon.t: wrong implementation" + ); + } +} diff --git a/packages/ethereum-contracts/test/foundry/utils/BatchLiquidator.t.sol b/packages/ethereum-contracts/test/foundry/utils/BatchLiquidator.t.sol index dfaeea61cf..11e966caab 100644 --- a/packages/ethereum-contracts/test/foundry/utils/BatchLiquidator.t.sol +++ b/packages/ethereum-contracts/test/foundry/utils/BatchLiquidator.t.sol @@ -2,28 +2,38 @@ pragma solidity 0.8.19; import { FoundrySuperfluidTester, SuperTokenV1Library } from "../FoundrySuperfluidTester.sol"; -import { ISuperToken, SuperToken, ISuperfluid, IConstantOutflowNFT, IConstantInflowNFT } from "../../../contracts/superfluid/SuperToken.sol"; +import { + ISuperfluid, + ISuperToken, + IConstantOutflowNFT, + IConstantInflowNFT, + IPoolAdminNFT, + IPoolMemberNFT, + SuperToken +} from "../../../contracts/superfluid/SuperToken.sol"; +import { ISuperfluidPool } from "../../../contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol"; import { BatchLiquidator } from "../../../contracts/utils/BatchLiquidator.sol"; import "forge-std/Test.sol"; contract NonTransferableST is SuperToken { - // transferFrom will always revert - constructor( - ISuperfluid host - ) - SuperToken(host, IConstantOutflowNFT(address(0)), IConstantInflowNFT(address(0))) // solhint-disable-next-line no-empty-blocks - { - } - - function transferFrom(address holder, address recipient, uint256 amount) public override returns (bool) { + // transferFrom will always revert + constructor(ISuperfluid host) + SuperToken( + host, + IConstantOutflowNFT(address(0)), + IConstantInflowNFT(address(0)), + IPoolAdminNFT(address(0)), + IPoolMemberNFT(address(0)) + ) // solhint-disable-next-line + // no-empty-blocks + { } + + function transferFrom(address holder, address recipient, uint256 amount) public override returns (bool) { revert(); } - function mintInternal( - address to, - uint256 amount - ) external { - _mint(msg.sender, to, amount, false /* invokeHook */, false /* requireReceptionAck */, "", ""); + function mintInternal(address to, uint256 amount) external { + _mint(msg.sender, to, amount, false, /* invokeHook */ false, /* requireReceptionAck */ "", ""); } } @@ -41,121 +51,278 @@ contract BatchLiquidatorTest is FoundrySuperfluidTester { function setUp() public override { super.setUp(); - batchLiquidator = new BatchLiquidator(address(sf.host), address(sf.cfa)); + batchLiquidator = new BatchLiquidator(address(sf.host)); badToken = new NonTransferableST(sf.host); } // Helpers - function _startStream(address sender, address receiver, int96 flowRate) internal { - vm.startPrank(sender); - superToken.createFlow(receiver, flowRate); + + function _transferAllToSink(address sender) internal { + _helperTransferAll(superToken, sender, admin); + } + + function _assertNoCFAFlow(address sender, address receiver) internal { + (, int96 flowRate,,) = sf.cfa.getFlow(superToken, sender, receiver); + assertEq(flowRate, 0, "BatchLiquidator: CFA Flowrate should be 0"); + } + + function _assertNoGDAFlow(address sender, ISuperfluidPool pool) internal { + int96 flowRate = sf.gda.getFlowRate(superToken, sender, pool); + assertEq(flowRate, 0, "BatchLiquidator: GDA Flowrate should be 0"); + } + + function _assertLiquidatorBalanceGreater(address _liqudidator, uint256 balanceBefore_) internal { + assertGt( + superToken.balanceOf(_liqudidator), + balanceBefore_, + "BatchLiquidator: SL - Balance should be greater than before" + ); + } + + function _createCFAFlowLiquidationData(address sender, address receiver) + internal + pure + returns (BatchLiquidator.FlowLiquidationData memory) + { + return BatchLiquidator.FlowLiquidationData({ + agreementOperation: BatchLiquidator.FlowType.ConstantFlowAgreement, + sender: sender, + receiver: receiver + }); + } + + function _createGDAFlowLiquidationData(address sender, ISuperfluidPool pool) + internal + pure + returns (BatchLiquidator.FlowLiquidationData memory) + { + return BatchLiquidator.FlowLiquidationData({ + agreementOperation: BatchLiquidator.FlowType.GeneralDistributionAgreement, + sender: sender, + receiver: address(pool) + }); + } + + function testCFAOnlySingleLiquidation() public { + _helperCreateFlow(superToken, alice, bob, FLOW_RATE); + _transferAllToSink(alice); + + vm.startPrank(liquidator); + uint256 balanceBefore = superToken.balanceOf(liquidator); + vm.warp(4 hours); // jump 4 hours + batchLiquidator.deleteFlow(address(superToken), _createCFAFlowLiquidationData(alice, bob)); + _assertNoCFAFlow(alice, bob); + + _assertLiquidatorBalanceGreater(liquidator, balanceBefore); vm.stopPrank(); } - function _transferAllToSink(address sender) internal { - vm.startPrank(sender); - superToken.transferAll(admin); + function testGDAOnlySingleLiquidation() public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice); + _helperUpdateMemberUnits(pool, alice, bob, 1); + _helperDistributeFlow(superToken, alice, alice, pool, FLOW_RATE); + + _transferAllToSink(alice); + + vm.startPrank(liquidator); + uint256 balanceBefore = superToken.balanceOf(liquidator); + vm.warp(4 hours); // jump 4 hours + batchLiquidator.deleteFlow(address(superToken), _createGDAFlowLiquidationData(alice, pool)); + _assertNoGDAFlow(alice, pool); + + _assertLiquidatorBalanceGreater(liquidator, balanceBefore); vm.stopPrank(); } - function _assertNoFlow(address sender, address receiver) internal { - (, int96 flow,,) = sf.cfa.getFlow(superToken, sender, receiver); - assertEq(flow, 0, "BatchLiquidator: Flow should be 0"); + function testCFAOnlySingleLiquidationRevert() public { + vm.startPrank(liquidator); + vm.expectRevert(); + batchLiquidator.deleteFlow(address(superToken), _createCFAFlowLiquidationData(alice, bob)); + vm.stopPrank(); } - function testSingleLiquidation() public { - _startStream(alice, bob, FLOW_RATE); + function testGDAOnlySingleLiquidationRevert() public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice); + vm.startPrank(liquidator); + vm.expectRevert(); + batchLiquidator.deleteFlow(address(superToken), _createGDAFlowLiquidationData(alice, pool)); + vm.stopPrank(); + } + + function testCFAOnlyBatchLiquidation() public { + _helperCreateFlow(superToken, alice, bob, FLOW_RATE); + _helperCreateFlow(superToken, carol, bob, FLOW_RATE); + _helperCreateFlow(superToken, dan, bob, FLOW_RATE); + _transferAllToSink(alice); + _transferAllToSink(carol); + _transferAllToSink(dan); vm.startPrank(liquidator); - uint256 balance = superToken.balanceOf(liquidator); + uint256 balanceBefore = superToken.balanceOf(liquidator); vm.warp(4 hours); // jump 4 hours - batchLiquidator.deleteFlow(address(superToken), alice, bob); - _assertNoFlow(alice, bob); + BatchLiquidator.FlowLiquidationData[] memory data = new BatchLiquidator.FlowLiquidationData[](3); + data[0] = _createCFAFlowLiquidationData(alice, bob); + data[1] = _createCFAFlowLiquidationData(carol, bob); + data[2] = _createCFAFlowLiquidationData(dan, bob); - assertTrue( - superToken.balanceOf(liquidator) > balance, "BatchLiquidator: SL - Balance should be greater than before" - ); + batchLiquidator.deleteFlows(address(superToken), data); + + _assertNoCFAFlow(alice, bob); + _assertNoCFAFlow(carol, bob); + _assertNoCFAFlow(dan, bob); + + _assertLiquidatorBalanceGreater(liquidator, balanceBefore); + vm.stopPrank(); + } + + function testGDAOnlyBatchLiquidation() public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice); + _helperUpdateMemberUnits(pool, alice, bob, 1); + _helperDistributeFlow(superToken, alice, alice, pool, FLOW_RATE); + _helperDistributeFlow(superToken, carol, carol, pool, FLOW_RATE); + _helperDistributeFlow(superToken, dan, dan, pool, FLOW_RATE); + + int96 flowRate = sf.gda.getFlowRate(superToken, alice, pool); + + _transferAllToSink(alice); + _transferAllToSink(carol); + _transferAllToSink(dan); + + vm.startPrank(liquidator); + uint256 balanceBefore = superToken.balanceOf(liquidator); + vm.warp(4 hours); // jump 4 hours + BatchLiquidator.FlowLiquidationData[] memory data = new BatchLiquidator.FlowLiquidationData[](3); + data[0] = _createGDAFlowLiquidationData(alice, pool); + data[1] = _createGDAFlowLiquidationData(carol, pool); + data[2] = _createGDAFlowLiquidationData(dan, pool); + batchLiquidator.deleteFlows(address(superToken), data); + + flowRate = sf.gda.getFlowRate(superToken, alice, pool); + + _assertNoGDAFlow(alice, pool); + _assertNoGDAFlow(carol, pool); + _assertNoGDAFlow(dan, pool); + + _assertLiquidatorBalanceGreater(liquidator, balanceBefore); vm.stopPrank(); } - function testSingleLiquidationRevert() public { + function testCFAOnlyBatchLiquidationWithToleratedRevert() public { + _helperCreateFlow(superToken, alice, bob, FLOW_RATE); + _helperCreateFlow(superToken, dan, bob, FLOW_RATE); + + _transferAllToSink(alice); + _transferAllToSink(dan); + vm.startPrank(liquidator); - vm.expectRevert(); - batchLiquidator.deleteFlow(address(superToken), alice, bob); + uint256 balanceBefore = superToken.balanceOf(liquidator); + vm.warp(4 hours); // jump 4 hours + BatchLiquidator.FlowLiquidationData[] memory data = new BatchLiquidator.FlowLiquidationData[](3); + data[0] = _createCFAFlowLiquidationData(alice, bob); + data[1] = _createCFAFlowLiquidationData(carol, bob); + data[2] = _createCFAFlowLiquidationData(dan, bob); + + batchLiquidator.deleteFlows(address(superToken), data); + _assertNoCFAFlow(alice, bob); + _assertNoCFAFlow(carol, bob); + _assertNoCFAFlow(dan, bob); + + _assertLiquidatorBalanceGreater(liquidator, balanceBefore); vm.stopPrank(); } - function testRevertIfArrayLengthsDontMatch() public { - address[] memory senders = new address[](8); - address[] memory receivers = new address[](7); - vm.expectRevert(BatchLiquidator.ARRAY_SIZES_DIFFERENT.selector); - batchLiquidator.deleteFlows(address(superToken), senders, receivers); + function testGDAOnlyBatchLiquidationWithToleratedRevert() public { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice); + _helperUpdateMemberUnits(pool, alice, bob, 1); + _helperDistributeFlow(superToken, alice, alice, pool, FLOW_RATE); + _helperDistributeFlow(superToken, dan, dan, pool, FLOW_RATE); + + int96 flowRate = sf.gda.getFlowRate(superToken, alice, pool); + + _transferAllToSink(alice); + _transferAllToSink(dan); + + vm.startPrank(liquidator); + uint256 balanceBefore = superToken.balanceOf(liquidator); + vm.warp(4 hours); // jump 4 hours + BatchLiquidator.FlowLiquidationData[] memory data = new BatchLiquidator.FlowLiquidationData[](3); + data[0] = _createGDAFlowLiquidationData(alice, pool); + data[1] = _createGDAFlowLiquidationData(carol, pool); + data[2] = _createGDAFlowLiquidationData(dan, pool); + + batchLiquidator.deleteFlows(address(superToken), data); + + flowRate = sf.gda.getFlowRate(superToken, alice, pool); + + _assertNoGDAFlow(alice, pool); + _assertNoGDAFlow(carol, pool); + _assertNoGDAFlow(dan, pool); + + _assertLiquidatorBalanceGreater(liquidator, balanceBefore); + vm.stopPrank(); } function testBatchLiquidation() public { - _startStream(alice, bob, FLOW_RATE); - _startStream(carol, bob, FLOW_RATE); - _startStream(dan, bob, FLOW_RATE); + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice); + _helperUpdateMemberUnits(pool, alice, bob, 1); + _helperDistributeFlow(superToken, alice, alice, pool, FLOW_RATE); + + _helperCreateFlow(superToken, carol, bob, FLOW_RATE); + _helperCreateFlow(superToken, dan, bob, FLOW_RATE); _transferAllToSink(alice); _transferAllToSink(carol); _transferAllToSink(dan); vm.startPrank(liquidator); - uint256 balance = superToken.balanceOf(liquidator); + uint256 balanceBefore = superToken.balanceOf(liquidator); vm.warp(4 hours); // jump 4 hours - address[] memory senders = new address[](3); - address[] memory receivers = new address[](3); - senders[0] = alice; - senders[1] = carol; - senders[2] = dan; - receivers[0] = bob; - receivers[1] = bob; - receivers[2] = bob; - - batchLiquidator.deleteFlows(address(superToken), senders, receivers); - _assertNoFlow(alice, bob); - _assertNoFlow(carol, bob); - _assertNoFlow(dan, bob); - - assertTrue( - superToken.balanceOf(liquidator) > balance, "BatchLiquidator: BL - Balance should be greater than before" - ); + BatchLiquidator.FlowLiquidationData[] memory data = new BatchLiquidator.FlowLiquidationData[](4); + data[0] = _createGDAFlowLiquidationData(alice, pool); + data[1] = _createCFAFlowLiquidationData(carol, bob); + data[2] = _createCFAFlowLiquidationData(dan, bob); + + batchLiquidator.deleteFlows(address(superToken), data); + + _assertNoGDAFlow(alice, pool); + _assertNoCFAFlow(carol, bob); + _assertNoCFAFlow(dan, bob); + + _assertLiquidatorBalanceGreater(liquidator, balanceBefore); vm.stopPrank(); } function testBatchLiquidationWithToleratedRevert() public { - _startStream(alice, bob, FLOW_RATE); - _startStream(dan, bob, FLOW_RATE); + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice); + _helperUpdateMemberUnits(pool, alice, bob, 1); + _helperDistributeFlow(superToken, alice, alice, pool, FLOW_RATE); + + _helperCreateFlow(superToken, dan, bob, FLOW_RATE); _transferAllToSink(alice); _transferAllToSink(dan); vm.startPrank(liquidator); - uint256 balance = superToken.balanceOf(liquidator); + uint256 balanceBefore = superToken.balanceOf(liquidator); vm.warp(4 hours); // jump 4 hours - address[] memory senders = new address[](3); - address[] memory receivers = new address[](3); - senders[0] = alice; - senders[1] = carol; // carol has no flow - senders[2] = dan; - receivers[0] = bob; - receivers[1] = bob; - receivers[2] = bob; - - batchLiquidator.deleteFlows(address(superToken), senders, receivers); - _assertNoFlow(alice, bob); - _assertNoFlow(carol, bob); - _assertNoFlow(dan, bob); - - assertTrue( - superToken.balanceOf(liquidator) > balance, "BatchLiquidator: BLR - Balance should be greater than before" - ); + BatchLiquidator.FlowLiquidationData[] memory data = new BatchLiquidator.FlowLiquidationData[](4); + + data[0] = _createGDAFlowLiquidationData(alice, pool); + data[1] = _createCFAFlowLiquidationData(carol, bob); + data[2] = _createCFAFlowLiquidationData(dan, bob); + + batchLiquidator.deleteFlows(address(superToken), data); + + _assertNoGDAFlow(alice, pool); + _assertNoCFAFlow(carol, bob); + _assertNoCFAFlow(dan, bob); + + _assertLiquidatorBalanceGreater(liquidator, balanceBefore); vm.stopPrank(); } - function testLiquidationWithCustomTokenRevert() public { + function testCFALiquidationWithCustomTokenRevert() public { NonTransferableST(address(badToken)).mintInternal(alice, 10 ether); vm.startPrank(alice); @@ -165,17 +332,16 @@ contract BatchLiquidatorTest is FoundrySuperfluidTester { vm.stopPrank(); vm.startPrank(liquidator); - batchLiquidator.deleteFlow(address(badToken), alice, bob); - _assertNoFlow(alice, bob); + BatchLiquidator.FlowLiquidationData memory data = _createCFAFlowLiquidationData(alice, bob); - assertTrue( - superToken.balanceOf(liquidator) == 0, "BatchLiquidator: SL - Balance should be 0 because of revert" - ); - vm.stopPrank(); + batchLiquidator.deleteFlow(address(badToken), data); + _assertNoCFAFlow(alice, bob); + assertTrue(superToken.balanceOf(liquidator) == 0, "BatchLiquidator: SL - Balance should be 0 because of revert"); + vm.stopPrank(); } - function testBatchLiquidationWithCustomTokenRevert() public { + function testCFABatchLiquidationWithCustomTokenRevert() public { NonTransferableST(address(badToken)).mintInternal(alice, 10 ether); NonTransferableST(address(badToken)).mintInternal(bob, 10 ether); @@ -193,15 +359,12 @@ contract BatchLiquidatorTest is FoundrySuperfluidTester { vm.startPrank(liquidator); - address[] memory senders = new address[](2); - address[] memory receivers = new address[](2); - senders[0] = alice; - senders[1] = bob; - receivers[0] = bob; - receivers[1] = carol; + BatchLiquidator.FlowLiquidationData[] memory data = new BatchLiquidator.FlowLiquidationData[](2); + data[0] = _createCFAFlowLiquidationData(alice, bob); + data[1] = _createCFAFlowLiquidationData(bob, carol); - batchLiquidator.deleteFlows(address(superToken), senders, receivers); - _assertNoFlow(alice, bob); - _assertNoFlow(bob, carol); + batchLiquidator.deleteFlows(address(superToken), data); + _assertNoCFAFlow(alice, bob); + _assertNoCFAFlow(bob, carol); } } diff --git a/packages/ethereum-contracts/test/foundry/utils/TOGA.t.sol b/packages/ethereum-contracts/test/foundry/utils/TOGA.t.sol index 42e1fbb8c1..d3db0b4c53 100644 --- a/packages/ethereum-contracts/test/foundry/utils/TOGA.t.sol +++ b/packages/ethereum-contracts/test/foundry/utils/TOGA.t.sol @@ -431,7 +431,8 @@ contract TOGAIntegrationTest is FoundrySuperfluidTester { function testMultiplePICsInParallel(uint256 bond) public { bond = _boundBondValue(bond); - (, ISuperToken superToken2) = sfDeployer.deployWrapperSuperToken("TEST2", "TEST2", 18, type(uint256).max); + (, ISuperToken superToken2) = + sfDeployer.deployWrapperSuperToken("TEST2", "TEST2", 18, type(uint256).max, address(0)); _helperDeal(superToken2, alice, INIT_SUPER_TOKEN_BALANCE); _helperDeal(superToken2, bob, INIT_SUPER_TOKEN_BALANCE); diff --git a/packages/ethereum-contracts/test/ops-scripts/deployment.test.js b/packages/ethereum-contracts/test/ops-scripts/deployment.test.js index ee85a72150..f718d5a168 100644 --- a/packages/ethereum-contracts/test/ops-scripts/deployment.test.js +++ b/packages/ethereum-contracts/test/ops-scripts/deployment.test.js @@ -555,6 +555,7 @@ contract("Embedded deployment scripts", (accounts) => { s.superfluid.address, s.superfluid.address, // a dead loop proxy [], + ZERO_ADDRESS, ZERO_ADDRESS ); } catch (err) { diff --git a/packages/ethereum-contracts/test/types.ts b/packages/ethereum-contracts/test/types.ts index 8ec12930be..dc59755f90 100644 --- a/packages/ethereum-contracts/test/types.ts +++ b/packages/ethereum-contracts/test/types.ts @@ -2,6 +2,7 @@ import {BigNumber} from "ethers"; import { ConstantFlowAgreementV1, + GeneralDistributionAgreementV1, IERC1820Registry, InstantDistributionAgreementV1, ISuperToken, @@ -115,6 +116,7 @@ export interface TestEnvironmentContracts { superfluid: SuperfluidMock; cfa: ConstantFlowAgreementV1; ida: InstantDistributionAgreementV1; + gda: GeneralDistributionAgreementV1; governance: TestGovernance; ISuperToken: ISuperToken; resolver: Resolver; diff --git a/packages/ethereum-contracts/testsuites/apps-contracts.ts b/packages/ethereum-contracts/testsuites/apps-contracts.ts index 1d094a9175..20edbb21fc 100644 --- a/packages/ethereum-contracts/testsuites/apps-contracts.ts +++ b/packages/ethereum-contracts/testsuites/apps-contracts.ts @@ -1,4 +1,5 @@ import "../test/contracts/apps/SuperTokenV1Library.CFA.test"; import "../test/contracts/apps/SuperTokenV1Library.IDA.test"; +import "../test/contracts/apps/SuperTokenV1Library.GDA.test"; import "../test/contracts/apps/CFAv1Library.test"; import "../test/contracts/apps/IDAv1Library.test"; \ No newline at end of file diff --git a/packages/hot-fuzz/README.md b/packages/hot-fuzz/README.md index 39017b9967..ddc1dac72e 100644 --- a/packages/hot-fuzz/README.md +++ b/packages/hot-fuzz/README.md @@ -64,9 +64,9 @@ contract YouSuperAppHotFuzz is HotFuzzBase { constructor() HotFuzzBase(10 /* nTesters */ ) { // ... setup your app _app = new YourApp(sf.host, sf.cfa, superToken); - initTesters(); + _initTesters(); ... - addAccount(address(_app)); + _addAccount(address(_app)); } ``` As a convention, the contract file name should be `YourApp.hott.sol`. diff --git a/packages/hot-fuzz/contracts/HotFuzzBase.sol b/packages/hot-fuzz/contracts/HotFuzzBase.sol index d5560d9acc..7f3d776eaa 100644 --- a/packages/hot-fuzz/contracts/HotFuzzBase.sol +++ b/packages/hot-fuzz/contracts/HotFuzzBase.sol @@ -17,6 +17,9 @@ import { } from "@superfluid-finance/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeployer.sol"; import "@superfluid-finance/ethereum-contracts/contracts/apps/CFAv1Library.sol"; import "@superfluid-finance/ethereum-contracts/contracts/apps/IDAv1Library.sol"; +import { + SuperTokenV1Library +} from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; import { IERC20, @@ -26,8 +29,8 @@ import { SuperfluidTester } from "./SuperfluidTester.sol"; - contract HotFuzzBase { + using SuperTokenV1Library for SuperToken; // constants uint private constant INIT_TOKEN_BALANCE = type(uint160).max; uint private constant INIT_SUPER_TOKEN_BALANCE = type(uint128).max; @@ -44,23 +47,26 @@ contract HotFuzzBase { SuperfluidTester[] internal testers; address[] internal otherAccounts; uint256 internal expectedTotalSupply = 0; + bool internal liquidationFails; constructor(uint nTesters_) { _sfDeployer = new SuperfluidFrameworkDeployer(); _sfDeployer.deployTestFramework(); sf = _sfDeployer.getFramework(); - (token, superToken) = _sfDeployer.deployWrapperSuperToken( - "HOTFuzz Token", "HOTT", 18, type(uint256).max - ); + (token, superToken) = + _sfDeployer.deployWrapperSuperToken("HOTFuzz Token", "HOTT", 18, type(uint256).max, address(0)); nTesters = nTesters_; otherAccounts = new address[](0); + + _addAccount(address(sf.gda)); + _addAccount(address(sf.toga)); } - function initTesters() virtual internal { + function _initTesters() virtual internal { testers = new SuperfluidTester[](nTesters); for (uint i = 0; i < nTesters; ++i) { - testers[i] = createTester(); + testers[i] = _createTester(); token.mint(address(testers[i]), INIT_TOKEN_BALANCE); testers[i].upgradeSuperToken(INIT_SUPER_TOKEN_BALANCE); expectedTotalSupply += INIT_SUPER_TOKEN_BALANCE; @@ -71,45 +77,59 @@ contract HotFuzzBase { * IHotFuzz implementation **************************************************************************/ - function createTester() + function _createTester() virtual internal returns (SuperfluidTester) { return new SuperfluidTester(sf, token, superToken); } - function addAccount(address a) + function _addAccount(address a) internal { otherAccounts.push(a); } - function listAccounts() + function _listAccounts() internal view returns (address[] memory accounts) { - accounts = new address[](nTesters + otherAccounts.length); + accounts = new address[](_numAccounts()); for (uint i = 0; i < nTesters; ++i) accounts[i] = address(testers[i]); for (uint i = 0; i < otherAccounts.length; ++i) accounts[i + nTesters] = otherAccounts[i]; } - function getOneTester(uint8 a) + function _numAccounts() internal view returns (uint256) { + return nTesters + otherAccounts.length; + } + + function _getOneTester(uint8 a) internal view - returns (SuperfluidTester testerA) + returns (SuperfluidTester tester) { - testerA = testers[a % nTesters]; + tester = testers[a % _numAccounts()]; } - function getTwoTesters(uint8 a, uint8 b) + /// @dev The testers returned may be the same + function _getTwoTesters(uint8 a, uint8 b) internal view returns (SuperfluidTester testerA, SuperfluidTester testerB) { - testerA = testers[a % nTesters]; - // avoid tester B to be the same as tester A - testerB = testers[((a % nTesters) + (b % (nTesters - 1))) % nTesters]; + testerA = _getOneTester(a); + testerB = _getOneTester(b); } - function superTokenBalanceOfNow(address a) internal view returns (int256 avb) { + /// @dev The testers returned may be the same + function _getThreeTesters(uint8 a, uint8 b, uint8 c) + internal view + returns (SuperfluidTester testerA, SuperfluidTester testerB, SuperfluidTester testerC) + { + testerA = _getOneTester(a); + testerB = _getOneTester(b); + testerC = _getOneTester(c); + } + + function _superTokenBalanceOfNow(address a) internal view returns (int256 avb) { (avb,,,) = superToken.realtimeBalanceOfNow(a); } @@ -124,7 +144,7 @@ contract HotFuzzBase { function echidna_check_liquiditySumInvariance() public view returns (bool) { int256 liquiditySum = 0; - address[] memory accounts = listAccounts(); + address[] memory accounts = _listAccounts(); for (uint i = 0; i < accounts.length; ++i) { (int256 avb, uint256 d, uint256 od, ) = superToken.realtimeBalanceOfNow(accounts[i]); // FIXME: correct formula @@ -138,11 +158,17 @@ contract HotFuzzBase { function echidna_check_netFlowRateSumInvariant() public view returns (bool) { int96 netFlowRateSum = 0; - address[] memory accounts = listAccounts(); + address[] memory accounts = _listAccounts(); for (uint i = 0; i < accounts.length; ++i) { - netFlowRateSum += sf.cfa.getNetFlow(superToken, accounts[i]); + netFlowRateSum += superToken.getNetFlowRate(accounts[i]); } assert(netFlowRateSum == 0); return netFlowRateSum == 0; } + + function echidna_check_validLiquidationNeverRevertsInvariant() public view returns (bool) { + bool liquidationNeverFails = !liquidationFails; + assert(liquidationNeverFails); + return liquidationNeverFails; + } } diff --git a/packages/hot-fuzz/contracts/SuperfluidTester.sol b/packages/hot-fuzz/contracts/SuperfluidTester.sol index c1534793a3..b6e1cf3e53 100644 --- a/packages/hot-fuzz/contracts/SuperfluidTester.sol +++ b/packages/hot-fuzz/contracts/SuperfluidTester.sol @@ -3,31 +3,44 @@ pragma solidity >= 0.8.0; import "@superfluid-finance/ethereum-contracts/contracts/superfluid/Superfluid.sol"; import "@superfluid-finance/ethereum-contracts/contracts/superfluid/SuperToken.sol"; +import {ISuperfluidPool} from + "@superfluid-finance/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol"; +import {PoolConfig} from + "@superfluid-finance/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; import "@superfluid-finance/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol"; import "@superfluid-finance/ethereum-contracts/contracts/agreements/InstantDistributionAgreementV1.sol"; import "@superfluid-finance/ethereum-contracts/contracts/apps/CFAv1Library.sol"; import "@superfluid-finance/ethereum-contracts/contracts/apps/IDAv1Library.sol"; +import {SuperTokenV1Library} from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; import "@superfluid-finance/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeployer.sol"; contract SuperfluidTester { + using SuperTokenV1Library for ISuperToken; SuperfluidFrameworkDeployer.Framework internal sf; IERC20 internal token; ISuperToken internal superToken; - using CFAv1Library for CFAv1Library.InitData; - using IDAv1Library for IDAv1Library.InitData; - - constructor ( - SuperfluidFrameworkDeployer.Framework memory sf_, - IERC20 token_, - ISuperToken superToken_) - { + constructor(SuperfluidFrameworkDeployer.Framework memory sf_, IERC20 token_, ISuperToken superToken_) { sf = sf_; token = token_; superToken = superToken_; } + // ERC20 Functions + function approve(address spender, uint256 amount) public { + superToken.approve(spender, amount); + } + + function transfer(address recipient, uint256 amount) public { + superToken.transfer(recipient, amount); + } + + function transferFrom(address sender, address recipient, uint256 amount) public { + superToken.transferFrom(sender, recipient, amount); + } + + // SuperToken Functions function upgradeSuperToken(uint256 amount) public { token.approve(address(superToken), amount); superToken.upgrade(amount); @@ -37,31 +50,168 @@ contract SuperfluidTester { superToken.downgrade(amount); } + function transferAll(address recipient) public { + superToken.transferAll(recipient); + } + + function increaseAllowance(address spender, uint256 addedValue) public { + superToken.increaseAllowance(spender, addedValue); + } + + function decreaseAllowance(address spender, uint256 subtractedValue) public { + superToken.decreaseAllowance(spender, subtractedValue); + } + + // CFA functions + function flow(address receiver, int96 flowRate) public { (, int96 currentFlowRate,,) = sf.cfa.getFlow(superToken, address(this), receiver); if (flowRate == 0) { - sf.cfaLib.deleteFlow(address(this), receiver, superToken); + superToken.deleteFlow(address(this), receiver); } else if (currentFlowRate == 0) { - sf.cfaLib.createFlow(receiver, superToken, flowRate); + superToken.createFlow(receiver, flowRate); } else { - sf.cfaLib.updateFlow(receiver, superToken, flowRate); + superToken.updateFlow(receiver, flowRate); } } + function cfaLiquidate(address sender, address receiver) public { + superToken.deleteFlow(sender, receiver); + } + + function setFlowPermissions( + address flowOperator, + bool allowCreate, + bool allowUpdate, + bool allowDelete, + int96 flowRateAllowance + ) public { + superToken.setFlowPermissions(flowOperator, allowCreate, allowUpdate, allowDelete, flowRateAllowance); + } + + function setMaxFlowPermissions(address flowOperator) public { + superToken.setMaxFlowPermissions(flowOperator); + } + + function revokeFlowPermissions(address flowOperator) public { + superToken.revokeFlowPermissions(flowOperator); + } + + function increaseFlowRateAllowance(address flowOperator, int96 addedFlowRateAllowance) public { + superToken.increaseFlowRateAllowance(flowOperator, addedFlowRateAllowance); + } + + function decreaseFlowRateAllowance(address flowOperator, int96 subtractedFlowRateAllowance) public { + superToken.decreaseFlowRateAllowance(flowOperator, subtractedFlowRateAllowance); + } + + function increaseFlowRateAllowanceWithPermissions( + address flowOperator, + uint8 permissionsToAdd, + int96 addedFlowRateAllowance + ) public { + superToken.increaseFlowRateAllowanceWithPermissions(flowOperator, permissionsToAdd, addedFlowRateAllowance); + } + + function decreaseFlowRateAllowanceWithPermissions( + address flowOperator, + uint8 permissionsToRemove, + int96 subtractedFlowRateAllowance + ) public { + superToken.decreaseFlowRateAllowanceWithPermissions( + flowOperator, permissionsToRemove, subtractedFlowRateAllowance + ); + } + + // IDA functions function createIndex(uint32 indexId) public { - sf.idaLib.createIndex(superToken, indexId); + superToken.createIndex(indexId); } function updateSubscriptionUnits(uint32 indexId, address subscriber, uint128 units) public { - sf.idaLib.updateSubscriptionUnits(superToken, indexId, subscriber, units); + superToken.updateSubscriptionUnits(indexId, subscriber, units); + } + + function updateIndex(uint32 indexId, uint128 indexValue) public { + superToken.updateIndexValue(indexId, indexValue); } function distribute(uint32 indexId, uint256 amount) public { - sf.idaLib.distribute(superToken, indexId, amount); + superToken.distribute(indexId, amount); } function approveSubscription(address publisher, uint32 indexId) public { - sf.idaLib.approveSubscription(superToken, publisher, indexId); + superToken.approveSubscription(publisher, indexId); } + function revokeSubscription(address publisher, uint32 indexId) public { + superToken.revokeSubscription(publisher, indexId); + } + + function deleteSubscription(address publisher, uint32 indexId, address subscriber) public { + superToken.deleteSubscription(publisher, indexId, subscriber); + } + + function claim(address publisher, uint32 indexId, address subscriber) public { + superToken.claim(publisher, indexId, subscriber); + } + + // GDA functions + function createPool(address admin, PoolConfig memory config) public returns (ISuperfluidPool pool) { + pool = superToken.createPool(admin, config); + } + + function connectPool(ISuperfluidPool pool) public { + superToken.connectPool(pool); + } + + function disconnectPool(ISuperfluidPool pool) public { + superToken.disconnectPool(pool); + } + + function distributeToPool(address from, ISuperfluidPool pool, uint256 requestedAmount) public { + superToken.distributeToPool(from, pool, requestedAmount); + } + + function distributeFlow(address from, ISuperfluidPool pool, int96 flowRate) public { + superToken.distributeFlow(from, pool, flowRate); + } + + function gdaLiquidate(address from, ISuperfluidPool pool) public { + superToken.distributeFlow(from, pool, 0); + } + + // SuperfluidPool + function updateMemberUnits(ISuperfluidPool pool, address member, uint128 units) public { + pool.updateMemberUnits(member, units); + } + + function claimAll(ISuperfluidPool pool) public { + pool.claimAll(); + } + + function claimAll(ISuperfluidPool pool, address memberAddress) public { + pool.claimAll(memberAddress); + } + + // SuperfluidPool-ERC20 + function transfer(ISuperfluidPool pool, address to, uint256 amount) public { + pool.transfer(to, amount); + } + + function transferFrom(ISuperfluidPool pool, address from, address to, uint256 amount) public { + pool.transferFrom(from, to, amount); + } + + function increaseAllowance(ISuperfluidPool pool, address spender, uint256 addedValue) public { + pool.increaseAllowance(spender, addedValue); + } + + function decreaseAllowance(ISuperfluidPool pool, address spender, uint256 subtractedValue) public { + pool.decreaseAllowance(spender, subtractedValue); + } + + function approve(ISuperfluidPool pool, address spender, uint256 amount) public { + pool.approve(spender, amount); + } } diff --git a/packages/hot-fuzz/contracts/superfluid-tests/ConstantFlowAgreementV1.hott.sol b/packages/hot-fuzz/contracts/superfluid-tests/ConstantFlowAgreementV1.hott.sol index ed8650e71b..680ed89373 100644 --- a/packages/hot-fuzz/contracts/superfluid-tests/ConstantFlowAgreementV1.hott.sol +++ b/packages/hot-fuzz/contracts/superfluid-tests/ConstantFlowAgreementV1.hott.sol @@ -2,26 +2,113 @@ // solhint-disable reason-string pragma solidity >= 0.8.0; +import {SuperToken} from "@superfluid-finance/ethereum-contracts/contracts/superfluid/SuperToken.sol"; +import {SuperTokenV1Library} from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; import "../HotFuzzBase.sol"; - abstract contract CFAHotFuzzMixin is HotFuzzBase { + using SuperTokenV1Library for SuperToken; + function createFlow(uint8 a, uint8 b, int64 flowRate) public { require(flowRate > 0); - (SuperfluidTester testerA, SuperfluidTester testerB) = getTwoTesters(a, b); + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); testerA.flow(address(testerB), int96(flowRate)); } function deleteFlow(uint8 a, uint8 b) public { - (SuperfluidTester testerA, SuperfluidTester testerB) = getTwoTesters(a, b); + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); testerA.flow(address(testerB), 0); } + + /// @notice testerA liquidates a flow from testerB to testerC + /// @dev testerA can be the same as testerB or testerC + function cfaLiquidateFlow(uint8 a, uint8 b, uint8 c) public { + (SuperfluidTester liquidator, SuperfluidTester sender, SuperfluidTester recipient) = _getThreeTesters(a, b, c); + + // we first check the condition for whether a flow exists + bool flowExists = superToken.getFlowRate(address(sender), address(recipient)) > 0; + + // then we ensure that the sender has a critical balance + (int256 availableBalance,,,) = superToken.realtimeBalanceOfNow(address(sender)); + bool isSenderCritical = availableBalance < 0; + + // if both conditions are met, a liquidation should occur without fail + bool isLiquidationValid = flowExists && isSenderCritical; + if (isLiquidationValid) { + // solhint-disable-next-line no-empty-blocks + try liquidator.cfaLiquidate(address(sender), address(recipient)) {} + catch { + liquidationFails = true; + } + } + } + + function setFlowPermissions( + uint8 a, + uint8 b, + bool allowCreate, + bool allowUpdate, + bool allowDelete, + int96 flowRateAllowance + ) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + + testerA.setFlowPermissions(address(testerB), allowCreate, allowUpdate, allowDelete, flowRateAllowance); + } + + function setMaxFlowPermissions(uint8 a, uint8 b) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + + testerA.setMaxFlowPermissions(address(testerB)); + } + + function revokeFlowPermissions(uint8 a, uint8 b) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + + testerA.revokeFlowPermissions(address(testerB)); + } + + function increaseFlowRateAllowance(uint8 a, uint8 b, int96 addedFlowRateAllowance) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + + testerA.increaseFlowRateAllowance(address(testerB), addedFlowRateAllowance); + } + + function decreaseFlowRateAllowance(uint8 a, uint8 b, int96 subtractedFlowRateAllowance) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + + testerA.decreaseFlowRateAllowance(address(testerB), subtractedFlowRateAllowance); + } + + function increaseFlowRateAllowanceWithPermissions( + uint8 a, + uint8 b, + uint8 permissionsToAdd, + int96 addedFlowRateAllowance + ) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + + testerA.increaseFlowRateAllowanceWithPermissions(address(testerB), permissionsToAdd, addedFlowRateAllowance); + } + + function decreaseFlowRateAllowanceWithPermissions( + uint8 a, + uint8 b, + uint8 permissionsToRemove, + int96 subtractedFlowRateAllowance + ) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + + testerA.decreaseFlowRateAllowanceWithPermissions( + address(testerB), permissionsToRemove, subtractedFlowRateAllowance + ); + } } contract CFAHotFuzz is CFAHotFuzzMixin { constructor() HotFuzzBase(10) { - initTesters(); + _initTesters(); } } diff --git a/packages/hot-fuzz/contracts/superfluid-tests/GDAHotFuzz.yaml b/packages/hot-fuzz/contracts/superfluid-tests/GDAHotFuzz.yaml new file mode 100644 index 0000000000..f663f5dc81 --- /dev/null +++ b/packages/hot-fuzz/contracts/superfluid-tests/GDAHotFuzz.yaml @@ -0,0 +1 @@ +testMode: "property" diff --git a/packages/hot-fuzz/contracts/superfluid-tests/GeneralDistributionAgreementV1.hott.sol b/packages/hot-fuzz/contracts/superfluid-tests/GeneralDistributionAgreementV1.hott.sol new file mode 100644 index 0000000000..b1ad780664 --- /dev/null +++ b/packages/hot-fuzz/contracts/superfluid-tests/GeneralDistributionAgreementV1.hott.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: AGPLv3 +// solhint-disable reason-string +pragma solidity >= 0.8.0; + +import {SuperToken} from "@superfluid-finance/ethereum-contracts/contracts/superfluid/SuperToken.sol"; +import {SuperTokenV1Library} from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; +import {ISuperfluidPool} from + "@superfluid-finance/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol"; +import {PoolConfig} from + "@superfluid-finance/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; +import {HotFuzzBase, SuperfluidTester} from "../HotFuzzBase.sol"; + +abstract contract GDAHotFuzzMixin is HotFuzzBase { + using SuperTokenV1Library for SuperToken; + + ISuperfluidPool[] public pools; + + function getRandomPool(uint8 input) public view returns (ISuperfluidPool pool) { + if (pools.length > 0) { + pool = pools[input % (pools.length - 1)]; + } + } + + function createPool(uint8 a, PoolConfig memory config) public { + (SuperfluidTester tester) = _getOneTester(a); + ISuperfluidPool pool = tester.createPool(address(tester), config); + _addPool(pool); + } + + function maybeConnectPool(bool doConnect, uint8 a, uint8 b) public { + (SuperfluidTester tester) = _getOneTester(a); + ISuperfluidPool pool = getRandomPool(b); + if (doConnect) { + tester.connectPool(pool); + } else { + tester.disconnectPool(pool); + } + } + + function distributeToPool(uint8 a, uint8 b, uint128 requestedAmount) public { + (SuperfluidTester tester) = _getOneTester(a); + ISuperfluidPool pool = getRandomPool(b); + + tester.distributeToPool(address(tester), pool, requestedAmount); + } + + function distributeFlow(uint8 a, uint8 b, uint8 c, int96 flowRate) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + ISuperfluidPool pool = getRandomPool(c); + + testerA.distributeFlow(address(testerB), pool, flowRate); + } + + /// @notice testerA liquidates a flow from testerB to pool + /// @dev testerA can be the same as testerB + function gdaLiquidateFlow(uint8 a, uint8 b, uint8 c) public { + (SuperfluidTester liquidator, SuperfluidTester distributor) = _getTwoTesters(a, b); + ISuperfluidPool pool = getRandomPool(c); + + // we first check the condition for whether a flow exists + bool flowExists = superToken.getFlowDistributionFlowRate(address(distributor), pool) > 0; + + // then we ensure that the sender has a critical balance + (int256 availableBalance,,,) = superToken.realtimeBalanceOfNow(address(distributor)); + bool isDistributorCritical = availableBalance < 0; + + // if both conditions are met, a liquidation should occur without fail + bool isLiquidationValid = flowExists && isDistributorCritical; + if (isLiquidationValid) { + // solhint-disable-next-line no-empty-blocks + try liquidator.gdaLiquidate(address(distributor), pool) {} + catch { + liquidationFails = true; + } + } + } + + function updateMemberUnits(uint8 a, uint8 b, uint128 units) public { + (SuperfluidTester tester) = _getOneTester(a); + ISuperfluidPool pool = getRandomPool(b); + + tester.updateMemberUnits(pool, address(tester), units); + } + + function poolTransfer(uint8 a, uint8 b, uint256 amount) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + ISuperfluidPool pool = getRandomPool(b); + + testerA.transfer(pool, address(testerB), amount); + } + + function poolTransferFrom(uint8 a, uint8 b, uint8 c, uint256 amount) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + (SuperfluidTester tester) = _getOneTester(c); + ISuperfluidPool pool = getRandomPool(b); + + testerA.transferFrom(pool, address(tester), address(testerB), amount); + } + + function poolIncreaseAllowance(uint8 a, uint8 b, uint256 addedValue) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + ISuperfluidPool pool = getRandomPool(b); + + testerA.increaseAllowance(pool, address(testerB), addedValue); + } + + function poolDecreaseAllowance(uint8 a, uint8 b, uint256 subtractedValue) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + ISuperfluidPool pool = getRandomPool(b); + + testerA.decreaseAllowance(pool, address(testerB), subtractedValue); + } + + function poolApprove(uint8 a, uint8 b, uint256 amount) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + ISuperfluidPool pool = getRandomPool(b); + + testerA.approve(pool, address(testerB), amount); + } + + function claimAll(uint8 a, uint8 b) public { + (SuperfluidTester tester) = _getOneTester(a); + ISuperfluidPool pool = getRandomPool(b); + + tester.claimAll(pool); + } + + function claimAllForMember(uint8 a, uint8 b) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + ISuperfluidPool pool = getRandomPool(b); + + testerA.claimAll(pool, address(testerB)); + } + + function _addPool(ISuperfluidPool pool) internal { + pools.push(pool); + _addAccount(address(pool)); + } +} + +contract GDAHotFuzz is HotFuzzBase(10), GDAHotFuzzMixin { + uint256 public constant NUM_POOLS = 3; + + constructor() { + _initTesters(); + + PoolConfig memory config = PoolConfig({transferabilityForUnitsOwner: true, distributionFromAnyAddress: true}); + + for (uint256 i; i < NUM_POOLS; i++) { + (SuperfluidTester tester) = _getOneTester(uint8(i)); + ISuperfluidPool pool = tester.createPool(address(tester), config); + _addPool(pool); + } + } +} diff --git a/packages/hot-fuzz/contracts/superfluid-tests/InstantDistributionAgreementV1.hott.sol b/packages/hot-fuzz/contracts/superfluid-tests/InstantDistributionAgreementV1.hott.sol index 704ebd251f..aadbb7080b 100644 --- a/packages/hot-fuzz/contracts/superfluid-tests/InstantDistributionAgreementV1.hott.sol +++ b/packages/hot-fuzz/contracts/superfluid-tests/InstantDistributionAgreementV1.hott.sol @@ -11,7 +11,7 @@ abstract contract IDAHotFuzzMixin is HotFuzzBase { function setupIndex(uint8 a, uint8 b, uint32 indexId, uint128 units) public { indexId = indexId % MAX_NUM_INDICES; - (SuperfluidTester testerA, SuperfluidTester testerB) = getTwoTesters(a, b); + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); (bool exists,,,) = sf.ida.getIndex(superToken, address(testerA), indexId); if (!exists) { @@ -22,23 +22,52 @@ abstract contract IDAHotFuzzMixin is HotFuzzBase { function distributeIfIndexExists(uint8 a, uint32 indexId, uint256 amount) public { indexId = indexId % MAX_NUM_INDICES; - (SuperfluidTester testerA) = getOneTester(a); + (SuperfluidTester testerA) = _getOneTester(a); (bool exists,,,) = sf.ida.getIndex(superToken, address(testerA), indexId); if (exists) { (uint256 actualAmount, ) = sf.ida.calculateDistribution(superToken, address(testerA), indexId, amount); + int256 a1 = _superTokenBalanceOfNow(address(testerA)); testerA.distribute(indexId, amount); - int256 a1 = superTokenBalanceOfNow(address(testerA)); - int256 a2 = superTokenBalanceOfNow(address(testerA)); + int256 a2 = _superTokenBalanceOfNow(address(testerA)); assert(a1 - a2 == int256(actualAmount)); } } + function updateIndexIfIndexExists(uint8 a, uint32 indexId, uint128 indexValue) public { + indexId = indexId % MAX_NUM_INDICES; + (SuperfluidTester testerA) = _getOneTester(a); + + (bool exists,,,) = sf.ida.getIndex(superToken, address(testerA), indexId); + if (exists) { + testerA.updateIndex(indexId, indexValue); + } + } + + function revokeSubscription(uint8 a, uint8 b, uint32 indexId) public { + indexId = indexId % MAX_NUM_INDICES; + + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + testerB.revokeSubscription(address(testerA), indexId); + } + + function deleteSubscription(uint8 a, uint8 b, uint32 indexId) public { + indexId = indexId % MAX_NUM_INDICES; + + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + testerB.deleteSubscription(address(testerB), indexId, address(testerA)); + } + + function claim(uint8 a, uint8 b, uint32 indexId) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + testerB.claim(address(testerB), indexId, address(testerA)); + } + function updateSubscriptionUnits(uint8 a, uint8 b, uint32 indexId, uint128 units) public { require(units > 0); indexId = indexId % MAX_NUM_INDICES; - (SuperfluidTester testerA, SuperfluidTester testerB) = getTwoTesters(a, b); + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); testerA.updateSubscriptionUnits(indexId, address(testerB), units); bool exist; @@ -51,7 +80,7 @@ abstract contract IDAHotFuzzMixin is HotFuzzBase { function approveSubscriptionUnits(uint8 a, uint8 b, uint32 indexId) public { indexId = indexId % MAX_NUM_INDICES; - (SuperfluidTester testerA, SuperfluidTester testerB) = getTwoTesters(a, b); + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); bool exist; bool approved; sf.ida.getSubscription(superToken, address(testerA), indexId, address(testerB)); @@ -65,6 +94,6 @@ abstract contract IDAHotFuzzMixin is HotFuzzBase { contract IDAHotFuzz is IDAHotFuzzMixin { constructor() HotFuzzBase(10) { - initTesters(); + _initTesters(); } } diff --git a/packages/hot-fuzz/contracts/superfluid-tests/SuperHotFuzz.sol b/packages/hot-fuzz/contracts/superfluid-tests/SuperHotFuzz.sol index 8485ee7dbc..300c89846e 100644 --- a/packages/hot-fuzz/contracts/superfluid-tests/SuperHotFuzz.sol +++ b/packages/hot-fuzz/contracts/superfluid-tests/SuperHotFuzz.sol @@ -3,12 +3,12 @@ pragma solidity >= 0.8.0; import "./ConstantFlowAgreementV1.hott.sol"; import "./InstantDistributionAgreementV1.hott.sol"; +import "./GeneralDistributionAgreementV1.hott.sol"; import "./SuperToken.hott.sol"; - // Combine all the hot fuzzes -contract SuperHotFuzz is CFAHotFuzzMixin, IDAHotFuzzMixin, SuperTokenHotFuzzMixin { - constructor() HotFuzzBase(10) { - initTesters(); +contract SuperHotFuzz is HotFuzzBase(10), CFAHotFuzzMixin, IDAHotFuzzMixin, GDAHotFuzzMixin, SuperTokenHotFuzzMixin { + constructor() { + _initTesters(); } } diff --git a/packages/hot-fuzz/contracts/superfluid-tests/SuperToken.hott.sol b/packages/hot-fuzz/contracts/superfluid-tests/SuperToken.hott.sol index bbf55855fc..d833ce9de4 100644 --- a/packages/hot-fuzz/contracts/superfluid-tests/SuperToken.hott.sol +++ b/packages/hot-fuzz/contracts/superfluid-tests/SuperToken.hott.sol @@ -4,16 +4,46 @@ pragma solidity >= 0.8.0; import "../HotFuzzBase.sol"; - abstract contract SuperTokenHotFuzzMixin is HotFuzzBase { + function approve(uint8 a, uint8 b, uint256 amount) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + testerA.approve(address(testerB), amount); + } + + function increaseAllowance(uint8 a, uint8 b, uint256 addedValue) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + testerA.increaseAllowance(address(testerB), addedValue); + } + + function decreaseAllowance(uint8 a, uint8 b, uint256 subtractedValue) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + testerA.decreaseAllowance(address(testerB), subtractedValue); + } + + function transfer(uint8 a, uint8 b, uint256 amount) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + testerA.transfer(address(testerB), amount); + } + + function transferFrom(uint8 a, uint8 b, uint8 c, uint256 amount) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + SuperfluidTester testerC = _getOneTester(c); + testerA.transferFrom(address(testerB), address(testerC), amount); + } + + function transferAll(uint8 a, uint8 b) public { + (SuperfluidTester testerA, SuperfluidTester testerB) = _getTwoTesters(a, b); + testerA.transferAll(address(testerB)); + } + function upgrade(uint8 a, uint64 amount) public { require(amount > 0); - SuperfluidTester tester = getOneTester(a); + SuperfluidTester tester = _getOneTester(a); - int256 a1 = superTokenBalanceOfNow(address(tester)); + int256 a1 = _superTokenBalanceOfNow(address(tester)); int256 b1 = int256(token.balanceOf(address(tester))); tester.upgradeSuperToken(amount); - int256 a2 = superTokenBalanceOfNow(address(tester)); + int256 a2 = _superTokenBalanceOfNow(address(tester)); int256 b2 = int256(token.balanceOf(address(tester))); assert(int256(uint256(amount)) == b1 - b2); assert(b1 - b2 == a2 - a1); @@ -22,13 +52,13 @@ abstract contract SuperTokenHotFuzzMixin is HotFuzzBase { function downgrade(uint8 a, uint64 amount) public { require(amount > 0); - SuperfluidTester tester = getOneTester(a); + SuperfluidTester tester = _getOneTester(a); - int256 a1 = superTokenBalanceOfNow(address(tester)); + int256 a1 = _superTokenBalanceOfNow(address(tester)); require(a1 >= int256(uint256(amount))); int256 b1 = int256(token.balanceOf(address(tester))); tester.downgradeSuperToken(amount); - int256 a2 = superTokenBalanceOfNow(address(tester)); + int256 a2 = _superTokenBalanceOfNow(address(tester)); int256 b2 = int256(token.balanceOf(address(tester))); assert(int256(uint256(amount)) == b2 - b1); assert(b2 - b1 == a1 - a2); @@ -38,6 +68,6 @@ abstract contract SuperTokenHotFuzzMixin is HotFuzzBase { contract SuperTokenHotFuzz is SuperTokenHotFuzzMixin { constructor() HotFuzzBase(10) { - initTesters(); + _initTesters(); } } diff --git a/packages/hot-fuzz/echidna.yaml b/packages/hot-fuzz/echidna.yaml index 75abbe03e5..814b007eca 100644 --- a/packages/hot-fuzz/echidna.yaml +++ b/packages/hot-fuzz/echidna.yaml @@ -7,24 +7,29 @@ cryticArgs: [ "--foundry-out-directory=build/foundry/out", # to generate: # $ (j=$((0xf01));tasks/list-all-linked-libraries.sh | while read i;do echo -n "($i,$(printf "0x%x" $j)),";j=$((j+1));done) - "--compile-libraries=(CFAv1ForwarderDeployerLibrary,0xf01),(IDAv1ForwarderDeployerLibrary,0xf02),(ProxyDeployerLibrary,0xf03),(SlotsBitmapLibrary,0xf04),(SuperfluidCFAv1DeployerLibrary,0xf05),(SuperfluidGovDeployerLibrary,0xf06),(SuperfluidHostDeployerLibrary,0xf07),(SuperfluidIDAv1DeployerLibrary,0xf08),(SuperfluidLoaderDeployerLibrary,0xf09),(SuperfluidNFTLogicDeployerLibrary,0xf0a),(SuperfluidPeripheryDeployerLibrary,0xf0b),(SuperTokenDeployerLibrary,0xf0c),(TokenDeployerLibrary,0xf0d)" + "--compile-libraries=(CFAv1ForwarderDeployerLibrary,0xf01),(GDAv1ForwarderDeployerLibrary,0xf02),(IDAv1ForwarderDeployerLibrary,0xf03),(ProxyDeployerLibrary,0xf04),(SlotsBitmapLibrary,0xf05),(SuperfluidCFAv1DeployerLibrary,0xf06),(SuperfluidFlowNFTLogicDeployerLibrary,0xf07),(SuperfluidGDAv1DeployerLibrary,0xf08),(SuperfluidGovDeployerLibrary,0xf09),(SuperfluidHostDeployerLibrary,0xf0a),(SuperfluidIDAv1DeployerLibrary,0xf0b),(SuperfluidLoaderDeployerLibrary,0xf0c),(SuperfluidPeripheryDeployerLibrary,0xf0d),(SuperfluidPoolDeployerLibrary,0xf0e),(SuperfluidPoolLogicDeployerLibrary,0xf0f),(SuperfluidPoolNFTLogicDeployerLibrary,0xf10),(SuperTokenDeployerLibrary,0xf11),(TokenDeployerLibrary,0xf12)" ] deployContracts: [ # to generate: # $ (j=$((0xf01));tasks/list-all-linked-libraries.sh | while read i;do echo "[\"$(printf "0x%x" $j)\", \"$i\"],";j=$((j+1));done) ["0xf01", "CFAv1ForwarderDeployerLibrary"], - ["0xf02", "IDAv1ForwarderDeployerLibrary"], - ["0xf03", "ProxyDeployerLibrary"], - ["0xf04", "SlotsBitmapLibrary"], - ["0xf05", "SuperfluidCFAv1DeployerLibrary"], - ["0xf06", "SuperfluidGovDeployerLibrary"], - ["0xf07", "SuperfluidHostDeployerLibrary"], - ["0xf08", "SuperfluidIDAv1DeployerLibrary"], - ["0xf09", "SuperfluidLoaderDeployerLibrary"], - ["0xf0a", "SuperfluidNFTLogicDeployerLibrary"], - ["0xf0b", "SuperfluidPeripheryDeployerLibrary"], - ["0xf0c", "SuperTokenDeployerLibrary"], - ["0xf0d", "TokenDeployerLibrary"], + ["0xf02", "GDAv1ForwarderDeployerLibrary"], + ["0xf03", "IDAv1ForwarderDeployerLibrary"], + ["0xf04", "ProxyDeployerLibrary"], + ["0xf05", "SlotsBitmapLibrary"], + ["0xf06", "SuperfluidCFAv1DeployerLibrary"], + ["0xf07", "SuperfluidFlowNFTLogicDeployerLibrary"], + ["0xf08", "SuperfluidGDAv1DeployerLibrary"], + ["0xf09", "SuperfluidGovDeployerLibrary"], + ["0xf0a", "SuperfluidHostDeployerLibrary"], + ["0xf0b", "SuperfluidIDAv1DeployerLibrary"], + ["0xf0c", "SuperfluidLoaderDeployerLibrary"], + ["0xf0d", "SuperfluidPeripheryDeployerLibrary"], + ["0xf0e", "SuperfluidPoolDeployerLibrary"], + ["0xf0f", "SuperfluidPoolLogicDeployerLibrary"], + ["0xf10", "SuperfluidPoolNFTLogicDeployerLibrary"], + ["0xf11", "SuperTokenDeployerLibrary"], + ["0xf12", "TokenDeployerLibrary"], ] deployBytecodes: [ ["0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24", "608060405234801561001057600080fd5b506109c5806100206000396000f3fe608060405234801561001057600080fd5b50600436106100a5576000357c010000000000000000000000000000000000000000000000000000000090048063a41e7d5111610078578063a41e7d51146101d4578063aabbb8ca1461020a578063b705676514610236578063f712f3e814610280576100a5565b806329965a1d146100aa5780633d584063146100e25780635df8122f1461012457806365ba36c114610152575b600080fd5b6100e0600480360360608110156100c057600080fd5b50600160a060020a038135811691602081013591604090910135166102b6565b005b610108600480360360208110156100f857600080fd5b5035600160a060020a0316610570565b60408051600160a060020a039092168252519081900360200190f35b6100e06004803603604081101561013a57600080fd5b50600160a060020a03813581169160200135166105bc565b6101c26004803603602081101561016857600080fd5b81019060208101813564010000000081111561018357600080fd5b82018360208201111561019557600080fd5b803590602001918460018302840111640100000000831117156101b757600080fd5b5090925090506106b3565b60408051918252519081900360200190f35b6100e0600480360360408110156101ea57600080fd5b508035600160a060020a03169060200135600160e060020a0319166106ee565b6101086004803603604081101561022057600080fd5b50600160a060020a038135169060200135610778565b61026c6004803603604081101561024c57600080fd5b508035600160a060020a03169060200135600160e060020a0319166107ef565b604080519115158252519081900360200190f35b61026c6004803603604081101561029657600080fd5b508035600160a060020a03169060200135600160e060020a0319166108aa565b6000600160a060020a038416156102cd57836102cf565b335b9050336102db82610570565b600160a060020a031614610339576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b6103428361092a565b15610397576040805160e560020a62461bcd02815260206004820152601a60248201527f4d757374206e6f7420626520616e204552433136352068617368000000000000604482015290519081900360640190fd5b600160a060020a038216158015906103b85750600160a060020a0382163314155b156104ff5760405160200180807f455243313832305f4143434550545f4d4147494300000000000000000000000081525060140190506040516020818303038152906040528051906020012082600160a060020a031663249cb3fa85846040518363ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018083815260200182600160a060020a0316600160a060020a031681526020019250505060206040518083038186803b15801561047e57600080fd5b505afa158015610492573d6000803e3d6000fd5b505050506040513d60208110156104a857600080fd5b5051146104ff576040805160e560020a62461bcd02815260206004820181905260248201527f446f6573206e6f7420696d706c656d656e742074686520696e74657266616365604482015290519081900360640190fd5b600160a060020a03818116600081815260208181526040808320888452909152808220805473ffffffffffffffffffffffffffffffffffffffff19169487169485179055518692917f93baa6efbd2244243bfee6ce4cfdd1d04fc4c0e9a786abd3a41313bd352db15391a450505050565b600160a060020a03818116600090815260016020526040812054909116151561059a5750806105b7565b50600160a060020a03808216600090815260016020526040902054165b919050565b336105c683610570565b600160a060020a031614610624576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b81600160a060020a031681600160a060020a0316146106435780610646565b60005b600160a060020a03838116600081815260016020526040808220805473ffffffffffffffffffffffffffffffffffffffff19169585169590951790945592519184169290917f605c2dbf762e5f7d60a546d42e7205dcb1b011ebc62a61736a57c9089d3a43509190a35050565b600082826040516020018083838082843780830192505050925050506040516020818303038152906040528051906020012090505b92915050565b6106f882826107ef565b610703576000610705565b815b600160a060020a03928316600081815260208181526040808320600160e060020a031996909616808452958252808320805473ffffffffffffffffffffffffffffffffffffffff19169590971694909417909555908152600284528181209281529190925220805460ff19166001179055565b600080600160a060020a038416156107905783610792565b335b905061079d8361092a565b156107c357826107ad82826108aa565b6107b85760006107ba565b815b925050506106e8565b600160a060020a0390811660009081526020818152604080832086845290915290205416905092915050565b6000808061081d857f01ffc9a70000000000000000000000000000000000000000000000000000000061094c565b909250905081158061082d575080155b1561083d576000925050506106e8565b61084f85600160e060020a031961094c565b909250905081158061086057508015155b15610870576000925050506106e8565b61087a858561094c565b909250905060018214801561088f5750806001145b1561089f576001925050506106e8565b506000949350505050565b600160a060020a0382166000908152600260209081526040808320600160e060020a03198516845290915281205460ff1615156108f2576108eb83836107ef565b90506106e8565b50600160a060020a03808316600081815260208181526040808320600160e060020a0319871684529091529020549091161492915050565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff161590565b6040517f01ffc9a7000000000000000000000000000000000000000000000000000000008082526004820183905260009182919060208160248189617530fa90519096909550935050505056fea165627a7a72305820377f4a2d4301ede9949f163f319021a6e9c687c292a5e2b2c4734c126b524e6c0029"], diff --git a/packages/hot-fuzz/package.json b/packages/hot-fuzz/package.json index 9e2f4244a1..c462bd46d0 100644 --- a/packages/hot-fuzz/package.json +++ b/packages/hot-fuzz/package.json @@ -25,7 +25,7 @@ "@superfluid-finance/ethereum-contracts": "1.8.0" }, "devDependencies": { - "@superfluid-finance/ethereum-contracts": "1.8.1" + "@superfluid-finance/ethereum-contracts": "1.9.0" }, "license": "AGPL-3.0", "bugs": { diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index e636244cb4..53d072a2ed 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -49,7 +49,7 @@ "node-fetch": "2.7.0" }, "devDependencies": { - "@superfluid-finance/ethereum-contracts": "1.8.1", + "@superfluid-finance/ethereum-contracts": "1.9.0", "chai-as-promised": "^7.1.1", "webpack": "^5.88.2", "webpack-bundle-analyzer": "^4.9.1", diff --git a/packages/metadata/main/networks/list.cjs b/packages/metadata/main/networks/list.cjs index 2878ce9c6f..46c14981ae 100644 --- a/packages/metadata/main/networks/list.cjs +++ b/packages/metadata/main/networks/list.cjs @@ -20,6 +20,7 @@ module.exports = "cfaV1Forwarder": "0xcfA132E353cB4E398080B9700609bb008eceB125", "idaV1": "0xfDdcdac21D64B639546f3Ce2868C7EF06036990c", "gdaV1": "0x3dB8Abd8B696F6c4150212A85961f954825Dd4B9", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0x94f26B4c8AD12B18c12f38E878618f7664bdcCE2", "constantOutflowNFT": "0xB18cbFeA12b5CB2626C74c94920dB1B37Ae91506", "constantInflowNFT": "0xF07df8b66ed80399B1E00981D61aD34EB4293032", @@ -81,6 +82,7 @@ module.exports = "cfaV1Forwarder": "0xcfA132E353cB4E398080B9700609bb008eceB125", "idaV1": "0x804348D4960a61f2d5F9ce9103027A3E849E09b8", "gdaV1": "0x63ab406B6eF6c8be732c1edbd15464de16a8F46D", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0xB798553db6EB3D3C56912378409370145E97324B", "constantOutflowNFT": "0x502CC982947216C0f94e433BC78c413806301C07", "constantInflowNFT": "0x9906A7e948C642B6bc74b9A5EAfCddB3580b44e0", @@ -144,6 +146,7 @@ module.exports = "cfaV1Forwarder": "0xcfA132E353cB4E398080B9700609bb008eceB125", "idaV1": "0x96215257F2FcbB00135578f766c0449d239bd92F", "gdaV1": "0xe87F46A15C410F151309Bf7516e130087Fc6a5E5", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0xfafe31cf998Df4e5D8310B03EBa8fb5bF327Eaf5", "constantOutflowNFT": "0xDF874BA132D8C68FEb5De513790f7612Fe20dDbd", "constantInflowNFT": "0xf88dd7208438Fdc5Ad05857eA701b7b51cdae0a9", @@ -186,6 +189,7 @@ module.exports = "cfaV1Forwarder": "0xcfA132E353cB4E398080B9700609bb008eceB125", "idaV1": "0x96215257F2FcbB00135578f766c0449d239bd92F", "gdaV1": "0xe87F46A15C410F151309Bf7516e130087Fc6a5E5", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0xfafe31cf998Df4e5D8310B03EBa8fb5bF327Eaf5", "constantOutflowNFT": "0xDF874BA132D8C68FEb5De513790f7612Fe20dDbd", "constantInflowNFT": "0xf88dd7208438Fdc5Ad05857eA701b7b51cdae0a9", @@ -227,10 +231,11 @@ module.exports = "cfaV1Forwarder": "0x2CDd45c5182602a36d391F7F16DD9f8386C3bD8D", "idaV1": "0xA44dEC7A0Dde1a56AeDe4143C1ef89cf5d956782", "gdaV1": "0x51f571D934C59185f13d17301a36c07A2268B814", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0x1C92042426B6bAAe497bEf461B6d8342D03aEc92", "constantOutflowNFT": "0x49583f57EFeBe733EC872c5d5437116085a3eE3c", "constantInflowNFT": "0x67d0Efab10b390206b356BA7FB453Ab56AAB7480", - "superfluidLoader": "0x96C3C2d23d143301cF363a02cB7fe3596d2834d7", + "superfluidLoader": "0x36446Ec9C7909608065dEB7f491701d815B880e5", "autowrap": { "manager": "0x30aE282CF477E2eF28B14d0125aCEAd57Fe1d7a1", "wrapStrategy": "0x1D65c6d3AD39d454Ea8F682c49aE7744706eA96d" diff --git a/packages/metadata/module/networks/list.js b/packages/metadata/module/networks/list.js index 7c012a9a9a..4e13384864 100644 --- a/packages/metadata/module/networks/list.js +++ b/packages/metadata/module/networks/list.js @@ -20,6 +20,7 @@ export default "cfaV1Forwarder": "0xcfA132E353cB4E398080B9700609bb008eceB125", "idaV1": "0xfDdcdac21D64B639546f3Ce2868C7EF06036990c", "gdaV1": "0x3dB8Abd8B696F6c4150212A85961f954825Dd4B9", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0x94f26B4c8AD12B18c12f38E878618f7664bdcCE2", "constantOutflowNFT": "0xB18cbFeA12b5CB2626C74c94920dB1B37Ae91506", "constantInflowNFT": "0xF07df8b66ed80399B1E00981D61aD34EB4293032", @@ -81,6 +82,7 @@ export default "cfaV1Forwarder": "0xcfA132E353cB4E398080B9700609bb008eceB125", "idaV1": "0x804348D4960a61f2d5F9ce9103027A3E849E09b8", "gdaV1": "0x63ab406B6eF6c8be732c1edbd15464de16a8F46D", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0xB798553db6EB3D3C56912378409370145E97324B", "constantOutflowNFT": "0x502CC982947216C0f94e433BC78c413806301C07", "constantInflowNFT": "0x9906A7e948C642B6bc74b9A5EAfCddB3580b44e0", @@ -144,6 +146,7 @@ export default "cfaV1Forwarder": "0xcfA132E353cB4E398080B9700609bb008eceB125", "idaV1": "0x96215257F2FcbB00135578f766c0449d239bd92F", "gdaV1": "0xe87F46A15C410F151309Bf7516e130087Fc6a5E5", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0xfafe31cf998Df4e5D8310B03EBa8fb5bF327Eaf5", "constantOutflowNFT": "0xDF874BA132D8C68FEb5De513790f7612Fe20dDbd", "constantInflowNFT": "0xf88dd7208438Fdc5Ad05857eA701b7b51cdae0a9", @@ -186,6 +189,7 @@ export default "cfaV1Forwarder": "0xcfA132E353cB4E398080B9700609bb008eceB125", "idaV1": "0x96215257F2FcbB00135578f766c0449d239bd92F", "gdaV1": "0xe87F46A15C410F151309Bf7516e130087Fc6a5E5", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0xfafe31cf998Df4e5D8310B03EBa8fb5bF327Eaf5", "constantOutflowNFT": "0xDF874BA132D8C68FEb5De513790f7612Fe20dDbd", "constantInflowNFT": "0xf88dd7208438Fdc5Ad05857eA701b7b51cdae0a9", @@ -227,10 +231,11 @@ export default "cfaV1Forwarder": "0x2CDd45c5182602a36d391F7F16DD9f8386C3bD8D", "idaV1": "0xA44dEC7A0Dde1a56AeDe4143C1ef89cf5d956782", "gdaV1": "0x51f571D934C59185f13d17301a36c07A2268B814", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0x1C92042426B6bAAe497bEf461B6d8342D03aEc92", "constantOutflowNFT": "0x49583f57EFeBe733EC872c5d5437116085a3eE3c", "constantInflowNFT": "0x67d0Efab10b390206b356BA7FB453Ab56AAB7480", - "superfluidLoader": "0x96C3C2d23d143301cF363a02cB7fe3596d2834d7", + "superfluidLoader": "0x36446Ec9C7909608065dEB7f491701d815B880e5", "autowrap": { "manager": "0x30aE282CF477E2eF28B14d0125aCEAd57Fe1d7a1", "wrapStrategy": "0x1D65c6d3AD39d454Ea8F682c49aE7744706eA96d" diff --git a/packages/metadata/networks.json b/packages/metadata/networks.json index d1028380c4..2e6414d381 100644 --- a/packages/metadata/networks.json +++ b/packages/metadata/networks.json @@ -18,6 +18,7 @@ "cfaV1Forwarder": "0xcfA132E353cB4E398080B9700609bb008eceB125", "idaV1": "0xfDdcdac21D64B639546f3Ce2868C7EF06036990c", "gdaV1": "0x3dB8Abd8B696F6c4150212A85961f954825Dd4B9", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0x94f26B4c8AD12B18c12f38E878618f7664bdcCE2", "constantOutflowNFT": "0xB18cbFeA12b5CB2626C74c94920dB1B37Ae91506", "constantInflowNFT": "0xF07df8b66ed80399B1E00981D61aD34EB4293032", @@ -79,6 +80,7 @@ "cfaV1Forwarder": "0xcfA132E353cB4E398080B9700609bb008eceB125", "idaV1": "0x804348D4960a61f2d5F9ce9103027A3E849E09b8", "gdaV1": "0x63ab406B6eF6c8be732c1edbd15464de16a8F46D", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0xB798553db6EB3D3C56912378409370145E97324B", "constantOutflowNFT": "0x502CC982947216C0f94e433BC78c413806301C07", "constantInflowNFT": "0x9906A7e948C642B6bc74b9A5EAfCddB3580b44e0", @@ -142,6 +144,7 @@ "cfaV1Forwarder": "0xcfA132E353cB4E398080B9700609bb008eceB125", "idaV1": "0x96215257F2FcbB00135578f766c0449d239bd92F", "gdaV1": "0xe87F46A15C410F151309Bf7516e130087Fc6a5E5", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0xfafe31cf998Df4e5D8310B03EBa8fb5bF327Eaf5", "constantOutflowNFT": "0xDF874BA132D8C68FEb5De513790f7612Fe20dDbd", "constantInflowNFT": "0xf88dd7208438Fdc5Ad05857eA701b7b51cdae0a9", @@ -184,6 +187,7 @@ "cfaV1Forwarder": "0xcfA132E353cB4E398080B9700609bb008eceB125", "idaV1": "0x96215257F2FcbB00135578f766c0449d239bd92F", "gdaV1": "0xe87F46A15C410F151309Bf7516e130087Fc6a5E5", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0xfafe31cf998Df4e5D8310B03EBa8fb5bF327Eaf5", "constantOutflowNFT": "0xDF874BA132D8C68FEb5De513790f7612Fe20dDbd", "constantInflowNFT": "0xf88dd7208438Fdc5Ad05857eA701b7b51cdae0a9", @@ -225,10 +229,11 @@ "cfaV1Forwarder": "0x2CDd45c5182602a36d391F7F16DD9f8386C3bD8D", "idaV1": "0xA44dEC7A0Dde1a56AeDe4143C1ef89cf5d956782", "gdaV1": "0x51f571D934C59185f13d17301a36c07A2268B814", + "gdaV1Forwarder": "0x6DA13Bde224A05a288748d857b9e7DDEffd1dE08", "superTokenFactory": "0x1C92042426B6bAAe497bEf461B6d8342D03aEc92", "constantOutflowNFT": "0x49583f57EFeBe733EC872c5d5437116085a3eE3c", "constantInflowNFT": "0x67d0Efab10b390206b356BA7FB453Ab56AAB7480", - "superfluidLoader": "0x96C3C2d23d143301cF363a02cB7fe3596d2834d7", + "superfluidLoader": "0x36446Ec9C7909608065dEB7f491701d815B880e5", "autowrap": { "manager": "0x30aE282CF477E2eF28B14d0125aCEAd57Fe1d7a1", "wrapStrategy": "0x1D65c6d3AD39d454Ea8F682c49aE7744706eA96d" diff --git a/packages/sdk-core/CHANGELOG.md b/packages/sdk-core/CHANGELOG.md index 5e2e52bbdd..f6b7afaffa 100644 --- a/packages/sdk-core/CHANGELOG.md +++ b/packages/sdk-core/CHANGELOG.md @@ -7,7 +7,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] ### Changed - - Map the name from subgraph to an unknown event, instead of "\_Unknown". ## [0.6.12] - 2023-10-23 diff --git a/packages/sdk-core/package.json b/packages/sdk-core/package.json index 831fcda00d..54ba7e620f 100644 --- a/packages/sdk-core/package.json +++ b/packages/sdk-core/package.json @@ -1,6 +1,6 @@ { "name": "@superfluid-finance/sdk-core", - "version": "0.6.12", + "version": "0.6.13", "description": "SDK Core for building with Superfluid Protocol", "homepage": "https://github.com/superfluid-finance/protocol-monorepo/tree/dev/packages/sdk-core#readme", "repository": { @@ -56,7 +56,7 @@ "url": "https://github.com/superfluid-finance/protocol-monorepo/issues" }, "dependencies": { - "@superfluid-finance/ethereum-contracts": "1.8.1", + "@superfluid-finance/ethereum-contracts": "1.9.0", "@superfluid-finance/metadata": "1.1.22", "browserify": "^17.0.0", "graphql-request": "^6.1.0", diff --git a/packages/sdk-core/src/ConstantFlowAgreementV1.ts b/packages/sdk-core/src/ConstantFlowAgreementV1.ts index 62260a7269..a978d55651 100644 --- a/packages/sdk-core/src/ConstantFlowAgreementV1.ts +++ b/packages/sdk-core/src/ConstantFlowAgreementV1.ts @@ -3,6 +3,7 @@ import { ethers } from "ethers"; import Host from "./Host"; import Operation from "./Operation"; import { SFError } from "./SFError"; +import SuperfluidAgreement from "./SuperfluidAgreement"; import { FlowRateAllowanceParams, FlowRateAllowanceWithPermissionsParams, @@ -40,7 +41,7 @@ const cfaInterface = IConstantFlowAgreementV1__factory.createInterface(); * Constant Flow Agreement V1 Helper Class * @description A helper class to interact with the CFAV1 contract. */ -export default class ConstantFlowAgreementV1 { +export default class ConstantFlowAgreementV1 extends SuperfluidAgreement { readonly host: Host; readonly contract: IConstantFlowAgreementV1; readonly forwarder: CFAv1Forwarder; @@ -50,6 +51,7 @@ export default class ConstantFlowAgreementV1 { cfaV1Address: string, cfaV1ForwarderAddress: string ) { + super(); this.host = new Host(hostAddress); this.contract = new ethers.Contract( cfaV1Address, @@ -743,26 +745,6 @@ export default class ConstantFlowAgreementV1 { /** ### Internal Helper Functions ### */ - /** - * Returns the desired Operation based on shouldUseCallAgreement. - * @param shouldUseCallAgreement whether or not to use host.callAgreement - * @param callAgreementOperation the host.callAgreement created Operation - * @param forwarderPopulatedTransactionPromise the populated forwarder transaction promise - */ - _getCallAgreementOperation = ( - callAgreementOperation: Operation, - forwarderPopulatedTransactionPromise?: Promise, - shouldUseCallAgreement?: boolean - ) => { - return shouldUseCallAgreement - ? callAgreementOperation - : new Operation( - callAgreementOperation.populateTransactionPromise, - callAgreementOperation.type, - forwarderPopulatedTransactionPromise - ); - }; - /** * Sanitizes flow info, converting BigNumber to string. * @param timestamp last updated timestamp of flow diff --git a/packages/sdk-core/src/Framework.ts b/packages/sdk-core/src/Framework.ts index f1102681d5..80893b60dd 100644 --- a/packages/sdk-core/src/Framework.ts +++ b/packages/sdk-core/src/Framework.ts @@ -4,6 +4,7 @@ import Web3 from "web3"; import BatchCall from "./BatchCall"; import ConstantFlowAgreementV1 from "./ConstantFlowAgreementV1"; +import GeneralDistributionAgreementV1 from "./GeneralDistributionAgreementV1"; import Governance from "./Governance"; import Host from "./Host"; import InstantDistributionAgreementV1 from "./InstantDistributionAgreementV1"; @@ -69,6 +70,7 @@ export default class Framework { governance: Governance; host: Host; idaV1: InstantDistributionAgreementV1; + gdaV1: GeneralDistributionAgreementV1; query: Query; private constructor( @@ -92,6 +94,11 @@ export default class Framework { settings.config.hostAddress, settings.config.idaV1Address ); + this.gdaV1 = new GeneralDistributionAgreementV1( + settings.config.hostAddress, + settings.config.gdaV1Address, + settings.config.gdaV1ForwarderAddress + ); this.query = new Query(settings); const resolver = new ethers.Contract( settings.config.resolverAddress, @@ -103,6 +110,7 @@ export default class Framework { governance: this.governance.contract, host: this.host.contract, idaV1: this.idaV1.contract, + gdaV1: this.gdaV1.contract, resolver, }; } @@ -191,6 +199,14 @@ export default class Framework { hostAddress: networkData.addresses.host, cfaV1Address: networkData.addresses.cfaV1, idaV1Address: networkData.addresses.idaV1, + // @note TODO - remove the any once you add gdaV1 and gdaV1Forwarder to metadata + // add idaV1Forwarder to metadata as well + gdaV1Address: + (networkData.addresses as any).gdaV1 || + networkData.addresses.idaV1, + gdaV1ForwarderAddress: + (networkData.addresses as any).gdaV1Forwarder || + networkData.addresses.idaV1, governanceAddress, cfaV1ForwarderAddress: networkData.addresses.cfaV1Forwarder, @@ -205,6 +221,8 @@ export default class Framework { ); const cfaV1ForwarderAddress = await resolver.get("CFAv1Forwarder"); + const gdaV1ForwarderAddress = + await resolver.get("GDAv1Forwarder"); const superfluidLoader = SuperfluidLoader__factory.connect( superfluidLoaderAddress, provider @@ -224,8 +242,10 @@ export default class Framework { hostAddress: framework.superfluid, cfaV1Address: framework.agreementCFAv1, idaV1Address: framework.agreementIDAv1, + gdaV1Address: framework.agreementGDAv1, governanceAddress, cfaV1ForwarderAddress, + gdaV1ForwarderAddress, }, }; diff --git a/packages/sdk-core/src/GeneralDistributionAgreementV1.ts b/packages/sdk-core/src/GeneralDistributionAgreementV1.ts new file mode 100644 index 0000000000..acd8563d85 --- /dev/null +++ b/packages/sdk-core/src/GeneralDistributionAgreementV1.ts @@ -0,0 +1,517 @@ +import { ethers } from "ethers"; + +import Host from "./Host"; +import { SFError } from "./SFError"; +import SuperfluidAgreement from "./SuperfluidAgreement"; +import SuperfluidPoolClass from "./SuperfluidPool"; +import { + ConnectPoolParams, + CreatePoolParams, + DisconnectPoolParams, + DistributeFlowParams, + DistributeParams, + EstimateDistributionActualAmountParams, + EstimateFlowDistributionActualFlowRateParams, + FlowDistributionActualFlowRateData, + GDAGetFlowRateParams, + GDAGetNetFlowParams, + GetPoolAdjustmentFlowInfoParams, + GetPoolAdjustmentFlowRateParams, + IsMemberConnectedParams, + IsPoolParams, + PoolAdjustmentFlowInfo, +} from "./interfaces"; +import { + GDAv1Forwarder, + GDAv1Forwarder__factory, + IGeneralDistributionAgreementV1, + IGeneralDistributionAgreementV1__factory, +} from "./typechain-types"; +import { normalizeAddress } from "./utils"; + +const gdaInterface = IGeneralDistributionAgreementV1__factory.createInterface(); + +/** + * General Distribution Agreement V1 Helper Class + * @description A helper class to interact with the GDAV1 contract. + */ +export default class GeneralDistributionAgreementV1 extends SuperfluidAgreement { + readonly host: Host; + readonly contract: IGeneralDistributionAgreementV1; + readonly forwarder: GDAv1Forwarder; + + constructor( + hostAddress: string, + gdaV1Address: string, + gdaV1ForwarderAddress: string + ) { + super(); + this.host = new Host(hostAddress); + this.contract = new ethers.Contract( + gdaV1Address, + IGeneralDistributionAgreementV1__factory.abi + ) as IGeneralDistributionAgreementV1; + this.forwarder = new ethers.Contract( + gdaV1ForwarderAddress, + GDAv1Forwarder__factory.abi + ) as GDAv1Forwarder; + } + + /** + * Retrieves the net flow for a specific token and account. + * + * @param token The token address. + * @param account The account address. + * @param providerOrSigner A provider or signer object + * @returns The net flow of the account for the token. + */ + getNetFlow = async (params: GDAGetNetFlowParams): Promise => { + const normalizedToken = normalizeAddress(params.token); + const normalizedAccount = normalizeAddress(params.account); + try { + return ( + await this.contract + .connect(params.providerOrSigner) + .getNetFlow(normalizedToken, normalizedAccount) + ).toString(); + } catch (err) { + throw new SFError({ + type: "GDAV1_READ", + message: "There was an error getting the GDA net flow.", + cause: err, + }); + } + }; + + /** + * Retrieves the flow rate for a specific token, sender, and pool. + * + * @param token The token address. + * @param from The sender address. + * @param pool The pool address. + * @param providerOrSigner A provider or signer object + * @returns The flow rate from the sender to the pool for the token. + */ + getFlowRate = async (params: GDAGetFlowRateParams): Promise => { + const normalizedToken = normalizeAddress(params.token); + const normalizedFrom = normalizeAddress(params.from); + const normalizedPool = normalizeAddress(params.pool); + + try { + return ( + await this.contract + .connect(params.providerOrSigner) + .getFlowRate( + normalizedToken, + normalizedFrom, + normalizedPool + ) + ).toString(); + } catch (err) { + throw new SFError({ + type: "GDAV1_READ", + message: "There was an error getting the GDA flow rate.", + cause: err, + }); + } + }; + + /** + * Estimates the flow distribution's actual flow rate for a specific token, sender, and pool. + * + * @param token The token address. + * @param from The sender address. + * @param pool The pool address. + * @param requestedFlowRate The requested flow rate. + * @param providerOrSigner A provider or signer object + * @returns The flow distribution's actual flow rate and the total distribution flow rate for the pool. + */ + estimateFlowDistributionActualFlowRate = async ( + params: EstimateFlowDistributionActualFlowRateParams + ): Promise => { + const normalizedToken = normalizeAddress(params.token); + const normalizedFrom = normalizeAddress(params.from); + const normalizedPool = normalizeAddress(params.pool); + + try { + const data = await this.contract + .connect(params.providerOrSigner) + .estimateFlowDistributionActualFlowRate( + normalizedToken, + normalizedFrom, + normalizedPool, + params.requestedFlowRate + ); + return { + actualFlowRate: data.actualFlowRate.toString(), + totalDistributionFlowRate: + data.totalDistributionFlowRate.toString(), + }; + } catch (err) { + throw new SFError({ + type: "GDAV1_READ", + message: + "There was an error estimating the GDA flow distribution's actual flow rate.", + cause: err, + }); + } + }; + + /** + * Estimates the distribution's actual amount for a specific token, sender, and pool. + * + * @param token The token address. + * @param from The sender address. + * @param pool The pool address. + * @param requestedAmount The requested amount. + * @param providerOrSigner A provider or signer object + * @returns The actual amount that will be distributed. + */ + estimateDistributionActualAmount = async ( + params: EstimateDistributionActualAmountParams + ): Promise => { + const normalizedToken = normalizeAddress(params.token); + const normalizedFrom = normalizeAddress(params.from); + const normalizedPool = normalizeAddress(params.pool); + try { + return ( + await this.contract + .connect(params.providerOrSigner) + .estimateDistributionActualAmount( + normalizedToken, + normalizedFrom, + normalizedPool, + params.requestedAmount + ) + ).toString(); + } catch (err) { + throw new SFError({ + type: "GDAV1_READ", + message: + "There was an error estimating the GDA distribution's actual amount.", + cause: err, + }); + } + }; + + /** + * Retrieves the pool adjustment flow rate for a specific token and pool. + * + * @param token The token address. + * @param pool The pool address. + * @param providerOrSigner A provider or signer object + * @returns The pool adjustment flow rate for the token and pool. + */ + getPoolAdjustmentFlowRate = async ( + params: GetPoolAdjustmentFlowRateParams + ): Promise => { + const normalizedPool = normalizeAddress(params.pool); + + try { + return ( + await this.contract + .connect(params.providerOrSigner) + .getPoolAdjustmentFlowRate(normalizedPool) + ).toString(); + } catch (err) { + throw new SFError({ + type: "GDAV1_READ", + message: + "There was an error getting the GDA pool adjustment flow rate.", + cause: err, + }); + } + }; + + /** + * Checks if a given token and account form a pool. + * + * @param token The token address. + * @param account The account address. + * @param providerOrSigner A provider or signer object + * @returns Whether the account is a pool for the token. + */ + isPool = async (params: IsPoolParams): Promise => { + const normalizedToken = normalizeAddress(params.token); + const normalizedAccount = normalizeAddress(params.account); + + try { + return await this.contract + .connect(params.providerOrSigner) + .isPool(normalizedToken, normalizedAccount); + } catch (err) { + throw new SFError({ + type: "GDAV1_READ", + message: + "There was an error checking if the account is a pool.", + cause: err, + }); + } + }; + + /** + * Checks if a member is connected to a specific pool. + * + * @param pool The pool address. + * @param member The member address. + * @param providerOrSigner A provider or signer object + * @returns Whether the member is connected to the pool. + */ + isMemberConnected = async ( + params: IsMemberConnectedParams + ): Promise => { + const normalizedPool = normalizeAddress(params.pool); + const normalizedMember = normalizeAddress(params.member); + + try { + return await this.contract + .connect(params.providerOrSigner) + .isMemberConnected(normalizedPool, normalizedMember); + } catch (err) { + throw new SFError({ + type: "GDAV1_READ", + message: + "There was an error checking if the member is connected to the pool.", + cause: err, + }); + } + }; + + /** + * Retrieves the pool adjustment flow information for a specific pool. + * + * @param pool The address of the pool. + * @param providerOrSigner A provider or signer object + * @returns The recipient of the pool adjustment flow, the flow hash and the rate of the adjustment flow. + */ + getPoolAdjustmentFlowInfo = async ( + params: GetPoolAdjustmentFlowInfoParams + ): Promise => { + const normalizedPool = normalizeAddress(params.pool); + + try { + const data = await this.contract + .connect(params.providerOrSigner) + .getPoolAdjustmentFlowInfo(normalizedPool); + return { + recipient: data[0], + flowHash: data[1], + flowRate: data[2].toString(), + }; + } catch (err) { + throw new SFError({ + type: "GDAV1_READ", + message: + "There was an error getting the GDA pool adjustment flow information.", + cause: err, + }); + } + }; + + /** + * Creates a new pool with the given token and admin. + * + * @param token The token address. + * @param admin The admin address. + * @returns CreatePoolTxn and SuperfluidPool instance + */ + createPool = async ( + params: CreatePoolParams + ): Promise<{ + createPoolTxn: ethers.ContractTransaction; + pool: SuperfluidPoolClass; + }> => { + const normalizedToken = normalizeAddress(params.token); + const normalizedAdmin = normalizeAddress(params.admin); + + try { + const createPoolTxn = await this.contract + .connect(params.signer) + .createPool(normalizedToken, normalizedAdmin, params.config); + const txnReceipt = await createPoolTxn.wait(); + const poolCreatedEvent = txnReceipt.events?.find( + (x) => x.event === "PoolCreated" + ); + const poolAddress = + poolCreatedEvent?.args?.pool || ethers.constants.AddressZero; + return { + createPoolTxn, + pool: new SuperfluidPoolClass(poolAddress), + }; + } catch (err) { + throw new SFError({ + type: "GDAV1_WRITE", + message: "There was an error creating the GDA pool.", + cause: err, + }); + } + }; + + /** + * Connects a pool to the contract. + * + * @param pool The pool address. + * @param userData The user data. + * @param overrides The transaction overrides. + * @returns The call agreement operation result. + */ + connectPool = (params: ConnectPoolParams) => { + const normalizedPool = normalizeAddress(params.pool); + const callData = gdaInterface.encodeFunctionData("connectPool", [ + normalizedPool, + "0x", + ]); + + const callAgreementOperation = this.host.callAgreement( + this.contract.address, + callData, + params.userData || "0x", + params.overrides + ); + + const forwarderPopulatedTxnPromise = + this.forwarder.populateTransaction.connectPool( + normalizedPool, + params.userData || "0x", + params.overrides || {} + ); + + return this._getCallAgreementOperation( + callAgreementOperation, + forwarderPopulatedTxnPromise, + params.shouldUseCallAgreement + ); + }; + + /** + * Disconnects a pool from the contract. + * + * @param pool The pool address. + * @param userData The user data. + * @param overrides The transaction overrides. + * @returns The call agreement operation result. + */ + disconnectPool = (params: DisconnectPoolParams) => { + const normalizedPool = normalizeAddress(params.pool); + const callData = gdaInterface.encodeFunctionData("disconnectPool", [ + normalizedPool, + "0x", + ]); + + const callAgreementOperation = this.host.callAgreement( + this.contract.address, + callData, + params.userData || "0x", + params.overrides + ); + + const forwarderPopulatedTxnPromise = + this.forwarder.populateTransaction.disconnectPool( + normalizedPool, + params.userData || "0x", + params.overrides || {} + ); + + return this._getCallAgreementOperation( + callAgreementOperation, + forwarderPopulatedTxnPromise, + params.shouldUseCallAgreement + ); + }; + + /** + * Distributes funds from the sender's account to the specified pool. + * + * @param token The token address. + * @param from The sender's address. + * @param pool The pool address. + * @param requestedAmount The requested amount to distribute. + * @param userData The user data. + * @param overrides The transaction overrides. + * @returns The call agreement operation result. + */ + distribute = (params: DistributeParams) => { + const normalizedToken = normalizeAddress(params.token); + const normalizedFrom = normalizeAddress(params.from); + const normalizedPool = normalizeAddress(params.pool); + + const callData = gdaInterface.encodeFunctionData("distribute", [ + normalizedToken, + normalizedFrom, + normalizedPool, + params.requestedAmount, + "0x", + ]); + + const callAgreementOperation = this.host.callAgreement( + this.contract.address, + callData, + params.userData || "0x", + params.overrides + ); + + const forwarderPopulatedTxnPromise = + this.forwarder.populateTransaction.distribute( + normalizedToken, + normalizedFrom, + normalizedPool, + params.requestedAmount, + params.userData || "0x", + params.overrides || {} + ); + + return this._getCallAgreementOperation( + callAgreementOperation, + forwarderPopulatedTxnPromise, + params.shouldUseCallAgreement + ); + }; + + /** + * Distributes the flow from the sender's account to the specified pool. + * + * @param token The token address. + * @param from The sender's address. + * @param pool The pool address. + * @param requestedFlowRate The requested flow rate. + * @param userData The user data. + * @param overrides The transaction overrides. + * @returns The call agreement operation result. + */ + distributeFlow = (params: DistributeFlowParams) => { + const normalizedToken = normalizeAddress(params.token); + const normalizedFrom = normalizeAddress(params.from); + const normalizedPool = normalizeAddress(params.pool); + + const callData = gdaInterface.encodeFunctionData("distributeFlow", [ + normalizedToken, + normalizedFrom, + normalizedPool, + params.requestedFlowRate, + "0x", + ]); + + const callAgreementOperation = this.host.callAgreement( + this.contract.address, + callData, + params.userData || "0x", + params.overrides + ); + + const forwarderPopulatedTxnPromise = + this.forwarder.populateTransaction.distributeFlow( + normalizedToken, + normalizedFrom, + normalizedPool, + params.requestedFlowRate, + params.userData || "0x", + params.overrides || {} + ); + + return this._getCallAgreementOperation( + callAgreementOperation, + forwarderPopulatedTxnPromise, + params.shouldUseCallAgreement + ); + }; +} diff --git a/packages/sdk-core/src/InstantDistributionAgreementV1.ts b/packages/sdk-core/src/InstantDistributionAgreementV1.ts index 58e046be9c..c889a8d9f4 100644 --- a/packages/sdk-core/src/InstantDistributionAgreementV1.ts +++ b/packages/sdk-core/src/InstantDistributionAgreementV1.ts @@ -3,6 +3,7 @@ import { ethers } from "ethers"; import Host from "./Host"; import Operation from "./Operation"; import { SFError } from "./SFError"; +import SuperfluidAgreement from "./SuperfluidAgreement"; import { IApproveSubscriptionParams, IClaimParams, @@ -29,11 +30,12 @@ const idaInterface = IInstantDistributionAgreementV1__factory.createInterface(); * Instant Distribution Agreement V1 Helper Class * @description A helper class to interact with the IDAV1 contract. */ -export default class InstantDistributionAgreementV1 { +export default class InstantDistributionAgreementV1 extends SuperfluidAgreement { readonly host: Host; readonly contract: IInstantDistributionAgreementV1; constructor(hostAddress: string, idaV1Address: string) { + super(); this.host = new Host(hostAddress); this.contract = new ethers.Contract( idaV1Address, diff --git a/packages/sdk-core/src/SFError.ts b/packages/sdk-core/src/SFError.ts index f3eec58e68..79623c85ff 100644 --- a/packages/sdk-core/src/SFError.ts +++ b/packages/sdk-core/src/SFError.ts @@ -8,6 +8,9 @@ export type ErrorType = | "CFAV1_READ" | "NFT_READ" | "IDAV1_READ" + | "GDAV1_READ" + | "SUPERFLUID_POOL_READ" + | "GDAV1_WRITE" | "INVALID_ADDRESS" | "INVALID_OBJECT" | "UNCLEAN_PERMISSIONS" @@ -24,6 +27,9 @@ const errorTypeToTitleMap = new Map([ ["SUPERTOKEN_READ", "SuperToken Read"], ["CFAV1_READ", "ConstantFlowAgreementV1 Read"], ["IDAV1_READ", "InstantDistributionAgreementV1 Read"], + ["GDAV1_READ", "GeneralDistributionAgreementV1 Read"], + ["GDAV1_WRITE", "GeneralDistributionAgreementV1 Write"], + ["SUPERFLUID_POOL_READ", "Superfluid Pool Read"], ["INVALID_ADDRESS", "Invalid Address"], ["INVALID_OBJECT", "Invalid Object"], ["UNSUPPORTED_OPERATION", "Unsupported Batch Call Operation"], diff --git a/packages/sdk-core/src/SuperToken.ts b/packages/sdk-core/src/SuperToken.ts index 1d7f67e889..b44286b5d4 100644 --- a/packages/sdk-core/src/SuperToken.ts +++ b/packages/sdk-core/src/SuperToken.ts @@ -4,6 +4,7 @@ import ConstantFlowAgreementV1 from "./ConstantFlowAgreementV1"; import ConstantInflowNFT from "./ConstantInflowNFT"; import ConstantOutflowNFT from "./ConstantOutflowNFT"; import ERC20Token from "./ERC20Token"; +import GeneralDistributionAgreementV1 from "./GeneralDistributionAgreementV1"; import Governance from "./Governance"; import InstantDistributionAgreementV1 from "./InstantDistributionAgreementV1"; import Operation from "./Operation"; @@ -11,11 +12,16 @@ import { SFError } from "./SFError"; import { chainIdToResolverDataMap, networkNameToChainIdMap } from "./constants"; import { getNetworkName } from "./frameworkHelpers"; import { + ConnectPoolParams, + DisconnectPoolParams, ERC20DecreaseAllowanceParams, ERC20IncreaseAllowanceParams, ERC777SendParams, + FlowDistributionActualFlowRateData, + GetPoolAdjustmentFlowInfoParams, IConfig, IRealtimeBalanceOfParams, + IsMemberConnectedParams, ISuperTokenBaseIDAParams, ISuperTokenCreateFlowByOperatorParams, ISuperTokenCreateFlowParams, @@ -41,8 +47,17 @@ import { IWeb3Index, IWeb3RealTimeBalanceOf, IWeb3Subscription, + SuperTokenCreatePoolParams, + SuperTokenDistributeFlowParams, + SuperTokenDistributeParams, + SuperTokenEstimateDistributionActualAmountParams, + SuperTokenEstimateDistributionActualFlowRateParams, SuperTokenFlowRateAllowanceParams, SuperTokenFlowRateAllowanceWithPermissionsParams, + SuperTokenGDAGetFlowRateParams, + SuperTokenGDAGetNetFlowParams, + SuperTokenGetPoolAdjustmentFlowRateParams, + SuperTokenIsPoolParams, } from "./interfaces"; import { ISETH, @@ -87,6 +102,7 @@ export default abstract class SuperToken extends ERC20Token { readonly settings: ITokenSettings; readonly cfaV1: ConstantFlowAgreementV1; readonly idaV1: InstantDistributionAgreementV1; + readonly gdaV1: GeneralDistributionAgreementV1; readonly governance: Governance; readonly underlyingToken?: ERC20Token; readonly constantOutflowNFTProxy?: ConstantOutflowNFT; @@ -110,6 +126,11 @@ export default abstract class SuperToken extends ERC20Token { settings.config.hostAddress, settings.config.idaV1Address ); + this.gdaV1 = new GeneralDistributionAgreementV1( + settings.config.hostAddress, + settings.config.gdaV1Address, + settings.config.gdaV1ForwarderAddress + ); this.governance = new Governance( settings.config.hostAddress, settings.config.governanceAddress @@ -797,6 +818,213 @@ export default abstract class SuperToken extends ERC20Token { }); }; + /** ### GDA Read Functions ### */ + + /** + * Retrieves the net flow for a specific token and account. + * + * @param account The account address. + * @param providerOrSigner A provider or signer object + * @returns The net flow of the account for the token. + */ + getGDANetFlow = async ( + params: SuperTokenGDAGetNetFlowParams + ): Promise => { + return this.gdaV1.getNetFlow({ + token: this.settings.address, + ...params, + }); + }; + + /** + * Retrieves the flow rate for a specific token, sender, and pool. + * + * @param from The sender address. + * @param pool The pool address. + * @param providerOrSigner A provider or signer object + * @returns The flow rate from the sender to the pool for the token. + */ + getFlowRate = async ( + params: SuperTokenGDAGetFlowRateParams + ): Promise => { + return this.gdaV1.getFlowRate({ + token: this.settings.address, + ...params, + }); + }; + + /** + * Estimates the flow distribution's actual flow rate for a specific token, sender, and pool. + * + * @param from The sender address. + * @param pool The pool address. + * @param requestedFlowRate The requested flow rate. + * @param providerOrSigner A provider or signer object + * @returns The flow distribution's actual flow rate and the total distribution flow rate for the pool. + */ + estimateFlowDistributionActualFlowRate = async ( + params: SuperTokenEstimateDistributionActualFlowRateParams + ): Promise => { + return this.gdaV1.estimateFlowDistributionActualFlowRate({ + token: this.settings.address, + ...params, + }); + }; + + /** + * Estimates the distribution's actual amount for a specific token, sender, and pool. + * + * @param from The sender address. + * @param pool The pool address. + * @param requestedAmount The requested amount. + * @param providerOrSigner A provider or signer object + * @returns The actual amount that will be distributed. + */ + estimateDistributionActualAmount = async ( + params: SuperTokenEstimateDistributionActualAmountParams + ): Promise => { + return this.gdaV1.estimateDistributionActualAmount({ + token: this.settings.address, + ...params, + }); + }; + + /** + * Retrieves the pool adjustment flow rate for a specific token and pool. + * + * @param pool The pool address. + * @param providerOrSigner A provider or signer object + * @returns The pool adjustment flow rate for the token and pool. + */ + getPoolAdjustmentFlowRate = async ( + params: SuperTokenGetPoolAdjustmentFlowRateParams + ): Promise => { + return this.gdaV1.getPoolAdjustmentFlowRate({ + ...params, + }); + }; + + /** + * Checks if a given token and account form a pool. + * + * @param account The account address. + * @param providerOrSigner A provider or signer object + * @returns Whether the account is a pool for the token. + */ + isPool = async (params: SuperTokenIsPoolParams): Promise => { + return this.gdaV1.isPool({ + token: this.settings.address, + ...params, + }); + }; + + /** + * Checks if a member is connected to a specific pool. + * + * @param pool The pool address. + * @param member The member address. + * @param providerOrSigner A provider or signer object + * @returns Whether the member is connected to the pool. + */ + isMemberConnected = async ( + params: IsMemberConnectedParams + ): Promise => { + return this.gdaV1.isMemberConnected({ + ...params, + }); + }; + + /** + * Retrieves the pool adjustment flow information for a specific pool. + * + * @param poolAddress The address of the pool. + * @param providerOrSigner A provider or signer object + * @returns The recipient of the pool adjustment flow, the flow hash and the rate of the adjustment flow. + */ + getPoolAdjustmentFlowInfo = async ( + params: GetPoolAdjustmentFlowInfoParams + ) => { + return this.gdaV1.getPoolAdjustmentFlowInfo(params); + }; + + /** ### GDA Write Functions ### */ + + /** + * Creates a new pool with the given token and admin. + * + * @param admin The admin address. + * @param overrides The transaction overrides. + * @returns The contract transaction and the pool address + */ + createPool = async (params: SuperTokenCreatePoolParams) => { + return await this.gdaV1.createPool({ + token: this.settings.address, + ...params, + }); + }; + + /** + * Connects a pool to the contract. + * + * @param pool The pool address. + * @param userData The user data. + * @param overrides The transaction overrides. + * @returns The call agreement operation result. + */ + connectPool = (params: ConnectPoolParams): Operation => { + return this.gdaV1.connectPool({ + ...params, + }); + }; + + /** + * Disconnects a pool from the contract. + * + * @param pool The pool address. + * @param userData The user data. + * @param overrides The transaction overrides. + * @returns The call agreement operation result. + */ + disconnectPool = (params: DisconnectPoolParams): Operation => { + return this.gdaV1.disconnectPool({ + ...params, + }); + }; + + /** + * Distributes funds from the sender's account to the specified pool. + * + * @param from The sender's address. + * @param pool The pool address. + * @param requestedAmount The requested amount to distribute. + * @param userData The user data. + * @param overrides The transaction overrides. + * @returns The call agreement operation result. + */ + distributeWithGDA = (params: SuperTokenDistributeParams): Operation => { + return this.gdaV1.distribute({ + token: this.settings.address, + ...params, + }); + }; + + /** + * Distributes the flow from the sender's account to the specified pool. + * + * @param from The sender's address. + * @param pool The pool address. + * @param requestedFlowRate The requested flow rate. + * @param userData The user data. + * @param overrides The transaction overrides. + * @returns The call agreement operation result. + */ + distributeFlow = (params: SuperTokenDistributeFlowParams): Operation => { + return this.gdaV1.distributeFlow({ + token: this.settings.address, + ...params, + }); + }; + /** ### Governance Read Functions ### */ getGovernanceParameters = async ( diff --git a/packages/sdk-core/src/SuperfluidAgreement.ts b/packages/sdk-core/src/SuperfluidAgreement.ts new file mode 100644 index 0000000000..ddc981bf85 --- /dev/null +++ b/packages/sdk-core/src/SuperfluidAgreement.ts @@ -0,0 +1,25 @@ +import { ethers } from "ethers"; + +import Operation from "./Operation"; + +export default class SuperfluidAgreement { + /** + * Returns the desired Operation based on shouldUseCallAgreement. + * @param shouldUseCallAgreement whether or not to use host.callAgreement + * @param callAgreementOperation the host.callAgreement created Operation + * @param forwarderPopulatedTransactionPromise the populated forwarder transaction promise + */ + _getCallAgreementOperation = ( + callAgreementOperation: Operation, + forwarderPopulatedTransactionPromise?: Promise, + shouldUseCallAgreement?: boolean + ) => { + return shouldUseCallAgreement + ? callAgreementOperation + : new Operation( + callAgreementOperation.populateTransactionPromise, + callAgreementOperation.type, + forwarderPopulatedTransactionPromise + ); + }; +} diff --git a/packages/sdk-core/src/SuperfluidPool.ts b/packages/sdk-core/src/SuperfluidPool.ts new file mode 100644 index 0000000000..62312f1cfe --- /dev/null +++ b/packages/sdk-core/src/SuperfluidPool.ts @@ -0,0 +1,478 @@ +import { ContractTransaction, ethers } from "ethers"; + +import { SFError } from "./SFError"; +import { + ClaimableData, + ClaimAllForMemberParams, + ERC20AllowanceParams, + ERC20ApproveParams, + ERC20BalanceOfParams, + ERC20TransferFromParams, + ERC20TransferParams, + GetClaimableNowParams, + GetClaimableParams, + GetDisconnectedBalanceParams, + GetMemberFlowRateParams, + GetUnitsParams, + SuperfluidPoolDecreaseAllowanceParams, + SuperfluidPoolIncreaseAllowanceParams, + UpdateMemberParams, +} from "./interfaces"; +import { ISuperfluidPool, ISuperfluidPool__factory } from "./typechain-types"; +import { normalizeAddress } from "./utils"; + +/** + * Superfluid Pool Helper Class + * @description A helper class to interact with the SuperfluidPool contract. + */ +export default class SuperfluidPoolClass { + readonly contract: ISuperfluidPool; + + constructor(poolAddress: string) { + this.contract = new ethers.Contract( + poolAddress, + ISuperfluidPool__factory.abi + ) as ISuperfluidPool; + } + + /** + * Retrieves the pool admin. + * @param providerOrSigner A provider or signer object + * @returns The pool admin. + */ + getPoolAdmin = async ( + providerOrSigner: ethers.providers.Provider | ethers.Signer + ): Promise => { + try { + return await this.contract.connect(providerOrSigner).admin(); + } catch (err) { + throw new SFError({ + type: "SUPERFLUID_POOL_READ", + message: "There was an error getting the pool admin.", + cause: err, + }); + } + }; + + /** + * Retrieves the SuperToken. + * @param providerOrSigner A provider or signer object + * @returns The SuperToken for this pool. + */ + getSuperToken = async ( + providerOrSigner: ethers.providers.Provider | ethers.Signer + ): Promise => { + try { + return await this.contract.connect(providerOrSigner).superToken(); + } catch (err) { + throw new SFError({ + type: "SUPERFLUID_POOL_READ", + message: "There was an error getting the pool's SuperToken.", + cause: err, + }); + } + }; + + /** + * Retrieves the total units. + * Returns the same value as totalSupply. + * @param providerOrSigner A provider or signer object + * @returns The total units. + */ + getTotalUnits = async ( + providerOrSigner: ethers.providers.Provider | ethers.Signer + ): Promise => { + try { + return ( + await this.contract.connect(providerOrSigner).getTotalUnits() + ).toString(); + } catch (err) { + throw new SFError({ + type: "SUPERFLUID_POOL_READ", + message: "There was an error getting total units.", + cause: err, + }); + } + }; + + /** + * Retrieves the total connected units. + * @param providerOrSigner A provider or signer object + * @returns The total connected units. + */ + getTotalConnectedUnits = async ( + providerOrSigner: ethers.providers.Provider | ethers.Signer + ): Promise => { + try { + return ( + await this.contract + .connect(providerOrSigner) + .getTotalConnectedUnits() + ).toString(); + } catch (err) { + throw new SFError({ + type: "SUPERFLUID_POOL_READ", + message: "There was an error getting total connected units.", + cause: err, + }); + } + }; + + /** + * Retrieves the units for a specific member. + * @param member The member's address. + * @param providerOrSigner A provider or signer object + * @returns The units for the specified member. + */ + getUnits = async (params: GetUnitsParams): Promise => { + try { + return ( + await this.contract + .connect(params.providerOrSigner) + .getUnits(params.member) + ).toString(); + } catch (err) { + throw new SFError({ + type: "SUPERFLUID_POOL_READ", + message: "There was an error getting units.", + cause: err, + }); + } + }; + + /** + * Retrieves the total connected flow rate. + * @param providerOrSigner A provider or signer object + * @returns The total connected flow rate. + */ + getTotalConnectedFlowRate = async ( + providerOrSigner: ethers.providers.Provider | ethers.Signer + ): Promise => { + try { + return ( + await this.contract + .connect(providerOrSigner) + .getTotalConnectedFlowRate() + ).toString(); + } catch (err) { + throw new SFError({ + type: "SUPERFLUID_POOL_READ", + message: + "There was an error getting total connected flow rate.", + cause: err, + }); + } + }; + + /** + * Retrieves the total disconnected flow rate. + * @param providerOrSigner A provider or signer object + * @returns The total disconnected flow rate. + */ + getTotalDisconnectedFlowRate = async ( + providerOrSigner: ethers.providers.Provider | ethers.Signer + ): Promise => { + try { + return ( + await this.contract + .connect(providerOrSigner) + .getTotalDisconnectedFlowRate() + ).toString(); + } catch (err) { + throw new SFError({ + type: "SUPERFLUID_POOL_READ", + message: + "There was an error getting total disconnected flow rate.", + cause: err, + }); + } + }; + + /** + * Retrieves the disconnected balance for the pool a specific time. + * @param time The time of the disconnected balance. + * @param providerOrSigner A provider or signer object + * @returns The disconnected balance. + */ + getDisconnectedBalance = async ( + params: GetDisconnectedBalanceParams + ): Promise => { + try { + return ( + await this.contract + .connect(params.providerOrSigner) + .getDisconnectedBalance(params.time) + ).toString(); + } catch (err) { + throw new SFError({ + type: "SUPERFLUID_POOL_READ", + message: "There was an error getting disconnected balance.", + cause: err, + }); + } + }; + + /** + * Retrieves the flow rate for a specific member. + * @param member The member's address. + * @param providerOrSigner A provider or signer object + * @returns The flow rate for the specified member. + */ + getMemberFlowRate = async ( + params: GetMemberFlowRateParams + ): Promise => { + try { + return ( + await this.contract + .connect(params.providerOrSigner) + .getMemberFlowRate(params.member) + ).toString(); + } catch (err) { + throw new SFError({ + type: "SUPERFLUID_POOL_READ", + message: "There was an error getting member flow rate.", + cause: err, + }); + } + }; + + /** + * Retrieves the claimable amount for a specific member and time. + * @param member The member's address. + * @param time The amount claimable at time. + * @param providerOrSigner A provider or signer object + * @returns The claimable amount. + */ + getClaimable = async (params: GetClaimableParams): Promise => { + const normalizedMember = normalizeAddress(params.member); + try { + return ( + await this.contract + .connect(params.providerOrSigner) + .getClaimable(normalizedMember, params.time) + ).toString(); + } catch (err) { + throw new SFError({ + type: "SUPERFLUID_POOL_READ", + message: "There was an error getting claimable amount.", + cause: err, + }); + } + }; + + /** + * Retrieves the claimable amount for a specific member at the current time. + * @param member The member's address. + * @param providerOrSigner A provider or signer object + * @returns ClaimableData: { timestamp, claimableBalance } + */ + getClaimableNow = async ( + params: GetClaimableNowParams + ): Promise => { + try { + const data = await this.contract + .connect(params.providerOrSigner) + .getClaimableNow(params.member); + return { + timestamp: data.timestamp.toString(), + claimableBalance: data.claimableBalance.toString(), + }; + } catch (err) { + throw new SFError({ + type: "SUPERFLUID_POOL_READ", + message: "There was an error getting claimable amount.", + cause: err, + }); + } + }; + + /** + * Updates the units for a specific member. + * @param member The member's address. + * @param newUnits The new units value. + * @param signer The transaction signer. + * @returns A promise that resolves when the update is complete. + */ + updateMemberUnits = async ( + params: UpdateMemberParams + ): Promise => { + const normalizedMember = normalizeAddress(params.member); + return await this.contract + .connect(params.signer) + .updateMemberUnits(normalizedMember, params.newUnits); + }; + + /** + * Claims all available funds for a specific member. + * @param member The member's address. + * @param signer The transaction signer. + * @returns A promise that resolves when the claim is complete. + */ + claimAllForMember = async ( + params: ClaimAllForMemberParams + ): Promise => { + return await this.contract + .connect(params.signer) + ["claimAll(address)"](params.member); + }; + + /** + * Claims all available funds. + * @returns A promise that resolves when the claim is complete. + */ + claimAll = async (signer: ethers.Signer): Promise => { + return await this.contract.connect(signer)["claimAll()"](); + }; + + /** + * Increases the allowance for a specific spender. + * @param spender The spender's address. + * @param amount The amount to increase the allowance by. + * @param signer The transaction signer. + * @returns A promise that resolves when the allowance increase is complete. + */ + increaseAllowance = async ( + params: SuperfluidPoolIncreaseAllowanceParams + ): Promise => { + const normalizedSpender = normalizeAddress(params.spender); + return await this.contract + .connect(params.signer) + .increaseAllowance(normalizedSpender, params.amount); + }; + + /** + * Decreases the allowance for a specific spender. + * @param spender The spender's address. + * @param amount The amount to decrease the allowance by. + * @param signer The transaction signer. + * @param overrides The transaction overrides. + * @returns A promise that resolves when the allowance decrease is complete. + */ + decreaseAllowance = async ( + params: SuperfluidPoolDecreaseAllowanceParams + ): Promise => { + const normalizedSpender = normalizeAddress(params.spender); + return await this.contract + .connect(params.signer) + .decreaseAllowance(normalizedSpender, params.amount); + }; + + /** + * Retrieves the total supply. + * Returns the same value as getTotalUnits. + * @returns The total supply. + */ + totalSupply = async ( + providerOrSigner: ethers.providers.Provider | ethers.Signer + ): Promise => { + try { + return ( + await this.contract.connect(providerOrSigner).totalSupply() + ).toString(); + } catch (err) { + throw new SFError({ + type: "SUPERFLUID_POOL_READ", + message: "There was an error getting total supply.", + cause: err, + }); + } + }; + + /** + * Retrieves the balance of an account. + * @param account The account's address. + * @param providerOrSigner A provider or signer object + * @returns The account's balance. + */ + balanceOf = async (params: ERC20BalanceOfParams): Promise => { + const normalizedAccount = normalizeAddress(params.account); + try { + return ( + await this.contract + .connect(params.providerOrSigner) + .balanceOf(normalizedAccount) + ).toString(); + } catch (err) { + throw new SFError({ + type: "SUPERFLUID_POOL_READ", + message: "There was an error getting balance.", + cause: err, + }); + } + }; + + /** + * Retrieves the allowance for a specific owner and spender. + * @param owner The owner's address. + * @param spender The spender's address. + * @param providerOrSigner A provider or signer object + * @returns The allowance. + */ + allowance = async (params: ERC20AllowanceParams): Promise => { + const normalizedOwner = normalizeAddress(params.owner); + const normalizedSpender = normalizeAddress(params.spender); + try { + return ( + await this.contract + .connect(params.providerOrSigner) + .allowance(normalizedOwner, normalizedSpender) + ).toString(); + } catch (err) { + throw new SFError({ + type: "SUPERFLUID_POOL_READ", + message: "There was an error getting allowance.", + cause: err, + }); + } + }; + + /** + * Approves an amount to be spent by a specific spender. + * @param spender The spender's address. + * @param amount The amount to approve. + * @param signer The transaction signer. + * @returns A promise that resolves when the approval is complete. + */ + approve = async ( + params: ERC20ApproveParams + ): Promise => { + const normalizedSpender = normalizeAddress(params.spender); + return await this.contract + .connect(params.signer) + .approve(normalizedSpender, params.amount); + }; + + /** + * Transfers an amount to a specific recipient. + * @param to The recipient's address. + * @param amount The amount to transfer. + * @param signer The transaction signer. + * @returns A promise that resolves when the transfer is complete. + */ + transfer = async ( + params: ERC20TransferParams + ): Promise => { + const normalizedTo = normalizeAddress(params.to); + return await this.contract + .connect(params.signer) + .transfer(normalizedTo, params.amount); + }; + + /** + * Transfers an amount from a specific sender to a recipient. + * @param from The sender's address. + * @param to The recipient's address. + * @param amount The amount to transfer. + * @param signer The transaction signer. + * @returns A promise that resolves when the transfer is complete. + */ + transferFrom = async ( + params: ERC20TransferFromParams + ): Promise => { + const normalizedFrom = normalizeAddress(params.from); + const normalizedTo = normalizeAddress(params.to); + return await this.contract + .connect(params.signer) + .transferFrom(normalizedFrom, normalizedTo, params.amount); + }; +} diff --git a/packages/sdk-core/src/index.ts b/packages/sdk-core/src/index.ts index 037dba5181..6834a8bc16 100644 --- a/packages/sdk-core/src/index.ts +++ b/packages/sdk-core/src/index.ts @@ -4,6 +4,7 @@ import BatchCall from "./BatchCall"; import ConstantFlowAgreementV1 from "./ConstantFlowAgreementV1"; import ERC20Token from "./ERC20Token"; import Framework from "./Framework"; +import GeneralDistributionAgreementV1 from "./GeneralDistributionAgreementV1"; import Governance from "./Governance"; import Host from "./Host"; import InstantDistributionAgreementV1 from "./InstantDistributionAgreementV1"; @@ -14,6 +15,7 @@ import SuperToken, { PureSuperToken, WrapperSuperToken, } from "./SuperToken"; +import SuperfluidPoolClass from "./SuperfluidPool"; export * from "./interfaces"; export * from "./constants"; @@ -32,6 +34,8 @@ export { Framework }; export { Governance }; export { Host }; export { InstantDistributionAgreementV1 }; +export { GeneralDistributionAgreementV1 }; +export { SuperfluidPoolClass }; export { NativeAssetSuperToken }; export { PureSuperToken }; export { Query }; diff --git a/packages/sdk-core/src/interfaces.ts b/packages/sdk-core/src/interfaces.ts index b2e532ee50..5207ecc4f2 100644 --- a/packages/sdk-core/src/interfaces.ts +++ b/packages/sdk-core/src/interfaces.ts @@ -2,11 +2,13 @@ import { ethers, Overrides } from "ethers"; import { IConstantFlowAgreementV1, + IGeneralDistributionAgreementV1, IInstantDistributionAgreementV1, IResolver, Superfluid, SuperfluidGovernanceII, } from "./typechain-types"; +import { PoolConfigStruct } from "./typechain-types/contracts/utils/GDAv1Forwarder"; // TODO (0xdavinchee): reorganize this // Maybe moving these into categorical files @@ -47,7 +49,7 @@ export interface ISuperTokenRequestFilter { // A better thought out inheritance pattern - SuperToken is parent // CFA/IDA inherits and tacks on superToken property -export interface IShouldUseCallAgreement { +export interface ShouldUseCallAgreement { readonly shouldUseCallAgreement?: boolean; } @@ -57,7 +59,7 @@ export interface EthersParams { // write request interfaces export interface ISuperTokenModifyFlowParams - extends IShouldUseCallAgreement, + extends ShouldUseCallAgreement, EthersParams { readonly flowRate?: string; readonly receiver: string; @@ -129,7 +131,7 @@ export interface ISuperTokenUpdateSubscriptionUnitsParams extends EthersParams { } export interface IModifyFlowParams - extends IShouldUseCallAgreement, + extends ShouldUseCallAgreement, EthersParams { readonly flowRate?: string; readonly receiver: string; @@ -168,13 +170,13 @@ export interface ISuperTokenFullControlParams extends EthersParams { export interface IUpdateFlowOperatorPermissionsParams extends ISuperTokenUpdateFlowOperatorPermissionsParams, - IShouldUseCallAgreement { + ShouldUseCallAgreement { readonly superToken: string; } export interface IFullControlParams extends ISuperTokenFullControlParams, - IShouldUseCallAgreement { + ShouldUseCallAgreement { readonly superToken: string; } @@ -447,8 +449,10 @@ export interface IConfig { readonly hostAddress: string; readonly cfaV1Address: string; readonly idaV1Address: string; + readonly gdaV1Address: string; readonly governanceAddress: string; readonly cfaV1ForwarderAddress: string; + readonly gdaV1ForwarderAddress: string; } export interface IContracts { @@ -456,6 +460,7 @@ export interface IContracts { readonly governance: SuperfluidGovernanceII; readonly host: Superfluid; readonly idaV1: IInstantDistributionAgreementV1; + readonly gdaV1: IGeneralDistributionAgreementV1; readonly resolver: IResolver; } @@ -518,14 +523,28 @@ export interface ERC20BalanceOfParams { readonly account: string; readonly providerOrSigner: ProviderOrSigner; } + export interface ERC20AllowanceParams { readonly owner: string; readonly spender: string; readonly providerOrSigner: ProviderOrSigner; } -export interface ERC20BalanceOfParams { - readonly account: string; - readonly providerOrSigner: ProviderOrSigner; + +export interface ERC20ApproveParams extends EthersParams { + readonly spender: string; + readonly amount: string; + readonly signer: ethers.Signer; +} + +export interface ERC20TransferParams extends EthersParams { + readonly to: string; + readonly amount: string; + readonly signer: ethers.Signer; +} + +export interface ERC20TransferFromParams extends ERC20TransferParams { + readonly from: string; + readonly signer: ethers.Signer; } // ERC721 @@ -581,6 +600,14 @@ export interface ERC20IncreaseAllowanceParams extends EthersParams { export type ERC20DecreaseAllowanceParams = ERC20IncreaseAllowanceParams; +export interface SuperfluidPoolIncreaseAllowanceParams + extends ERC20IncreaseAllowanceParams { + readonly signer: ethers.Signer; +} + +export type SuperfluidPoolDecreaseAllowanceParams = + SuperfluidPoolIncreaseAllowanceParams; + export interface SuperTokenFlowRateAllowanceParams extends EthersParams { readonly flowOperator: string; readonly flowRateAllowanceDelta: string; @@ -591,6 +618,176 @@ export interface FlowRateAllowanceParams readonly superToken: string; } +export interface SuperTokenGDAGetNetFlowParams { + readonly account: string; + readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; +} + +export interface GDAGetNetFlowParams extends SuperTokenGDAGetNetFlowParams { + readonly token: string; +} + +export interface SuperTokenGDAGetFlowRateParams { + readonly from: string; + readonly pool: string; + readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; +} + +export interface GDAGetFlowRateParams extends SuperTokenGDAGetFlowRateParams { + readonly token: string; +} + +export interface SuperTokenEstimateDistributionActualFlowRateParams { + readonly from: string; + readonly pool: string; + readonly requestedFlowRate: string; + readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; +} + +export interface EstimateFlowDistributionActualFlowRateParams + extends SuperTokenEstimateDistributionActualFlowRateParams { + readonly token: string; +} + +export interface SuperTokenEstimateDistributionActualAmountParams { + readonly from: string; + readonly pool: string; + readonly requestedAmount: string; + readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; +} +export interface EstimateDistributionActualAmountParams + extends SuperTokenEstimateDistributionActualAmountParams { + readonly token: string; +} + +export interface SuperTokenGetPoolAdjustmentFlowRateParams { + readonly pool: string; + readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; +} + +export interface GetPoolAdjustmentFlowRateParams + extends SuperTokenGetPoolAdjustmentFlowRateParams {} + +export interface SuperTokenIsPoolParams { + readonly account: string; + readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; +} + +export interface IsPoolParams extends SuperTokenIsPoolParams { + readonly token: string; +} + +export interface IsMemberConnectedParams { + readonly pool: string; + readonly member: string; + readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; +} + +export interface GetPoolAdjustmentFlowInfoParams { + readonly pool: string; + readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; +} + +export interface PoolAdjustmentFlowInfo { + readonly recipient: string; + readonly flowRate: string; + readonly flowHash: string; +} + +export interface SuperTokenCreatePoolParams { + readonly admin: string; + readonly config: PoolConfigStruct; + readonly signer: ethers.Signer; +} +export interface CreatePoolParams extends SuperTokenCreatePoolParams { + readonly token: string; +} + +export interface ConnectPoolParams + extends EthersParams, + ShouldUseCallAgreement { + readonly pool: string; + readonly userData?: string; +} + +export interface DisconnectPoolParams + extends EthersParams, + ShouldUseCallAgreement { + readonly pool: string; + readonly userData?: string; +} + +export interface SuperTokenDistributeParams + extends EthersParams, + ShouldUseCallAgreement { + readonly from: string; + readonly pool: string; + readonly requestedAmount: string; + readonly userData?: string; +} +export interface DistributeParams extends SuperTokenDistributeParams { + readonly token: string; +} + +export interface SuperTokenDistributeFlowParams + extends EthersParams, + ShouldUseCallAgreement { + readonly from: string; + readonly pool: string; + readonly requestedFlowRate: string; + readonly userData?: string; +} + +export interface DistributeFlowParams extends SuperTokenDistributeFlowParams { + readonly token: string; +} + +export interface FlowDistributionActualFlowRateData { + readonly actualFlowRate: string; + readonly totalDistributionFlowRate: string; +} + +export interface GetClaimableParams { + readonly member: string; + readonly time: string; + readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; +} + +export interface GetClaimableNowParams { + readonly member: string; + readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; +} + +export interface ClaimableData { + readonly claimableBalance: string; + readonly timestamp: string; +} + +export interface GetUnitsParams { + readonly member: string; + readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; +} + +export interface GetDisconnectedBalanceParams { + readonly time: string; + readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; +} + +export interface GetMemberFlowRateParams { + readonly member: string; + readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; +} + +export interface ClaimAllForMemberParams { + readonly member: string; + readonly signer: ethers.Signer; +} + +export interface UpdateMemberParams { + readonly member: string; + readonly newUnits: string; + readonly signer: ethers.Signer; +} export interface FlowRateAllowanceWithPermissionsParams extends FlowRateAllowanceParams { readonly permissionsDelta: number; diff --git a/packages/sdk-core/test/1.4_supertoken_gda.test.ts b/packages/sdk-core/test/1.4_supertoken_gda.test.ts new file mode 100644 index 0000000000..c0043e7542 --- /dev/null +++ b/packages/sdk-core/test/1.4_supertoken_gda.test.ts @@ -0,0 +1,1061 @@ +import { expect } from "chai"; +import { + TestEnvironment, + makeSuite, + validateOperationShouldUseCallAgreement, +} from "./TestEnvironment"; +import SuperfluidPool from "../src/SuperfluidPool"; +import { ethers } from "ethers"; +import { WrapperSuperToken, toBN } from "../src"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +interface ShouldConnectPoolParams { + testEnv: TestEnvironment; + shouldUseCallAgreement: boolean; + superToken: WrapperSuperToken; + pool: SuperfluidPool; + member: SignerWithAddress; + doConnect: boolean; +} + +interface ShouldUpdateMemberParams { + pool: SuperfluidPool; + newUnits: string; + member: SignerWithAddress; + admin: SignerWithAddress; +} + +interface ShouldClaimAllForMemberParams { + pool: SuperfluidPool; + member: SignerWithAddress; + claimer: SignerWithAddress; + superToken: WrapperSuperToken; + claimAll?: boolean; +} + +interface ShouldInstantDistributeParams { + testEnv: TestEnvironment; + shouldUseCallAgreement: boolean; + newUnits: string; + amountToDistribute: string; + admin: SignerWithAddress; + distributor: SignerWithAddress; + member: SignerWithAddress; +} + +interface ShouldFlowDistributeParams { + testEnv: TestEnvironment; + shouldUseCallAgreement: boolean; + newUnits: string; + requestedFlowRate: string; + admin: SignerWithAddress; + distributor: SignerWithAddress; + member: SignerWithAddress; + superToken: WrapperSuperToken; +} + +makeSuite( + "SuperToken-GDA and SuperfluidPool Tests", + (testEnv: TestEnvironment) => { + describe("Revert cases", () => { + it("Should throw an error on GDA view functions when wrong params passed", async () => { + try { + await testEnv.wrapperSuperToken.getGDANetFlow({ + account: "", + providerOrSigner: testEnv.alice, + }); + } catch (err) { + expect(err.type).to.equal("GDAV1_READ"); + expect(err.message).to.have.string( + "There was an error getting the GDA net flow." + ); + } + + try { + await testEnv.wrapperSuperToken.getFlowRate({ + from: "", + pool: "", + providerOrSigner: testEnv.alice, + }); + } catch (err) { + expect(err.type).to.equal("GDAV1_READ"); + expect(err.message).to.have.string( + "There was an error getting the GDA flow rate." + ); + } + + try { + await testEnv.wrapperSuperToken.estimateFlowDistributionActualFlowRate( + { + from: "", + pool: "", + requestedFlowRate: "", + providerOrSigner: testEnv.alice, + } + ); + } catch (err) { + expect(err.type).to.equal("GDAV1_READ"); + expect(err.message).to.have.string( + "There was an error estimating the GDA flow distribution's actual flow rate." + ); + } + + try { + await testEnv.wrapperSuperToken.estimateDistributionActualAmount( + { + from: "", + pool: "", + requestedAmount: "", + providerOrSigner: testEnv.alice, + } + ); + } catch (err) { + expect(err.type).to.equal("GDAV1_READ"); + expect(err.message).to.have.string( + "There was an error estimating the GDA distribution's actual amount." + ); + } + + try { + await testEnv.wrapperSuperToken.getPoolAdjustmentFlowRate({ + pool: "", + providerOrSigner: testEnv.alice, + }); + } catch (err) { + expect(err.type).to.equal("GDAV1_READ"); + expect(err.message).to.have.string( + "There was an error getting the GDA pool adjustment flow rate." + ); + } + + try { + await testEnv.wrapperSuperToken.isPool({ + account: "", + providerOrSigner: testEnv.alice, + }); + } catch (err) { + expect(err.type).to.equal("GDAV1_READ"); + expect(err.message).to.have.string( + "There was an error checking if the account is a pool." + ); + } + + try { + await testEnv.wrapperSuperToken.isMemberConnected({ + pool: "", + member: "", + providerOrSigner: testEnv.alice, + }); + } catch (err) { + expect(err.type).to.equal("GDAV1_READ"); + expect(err.message).to.have.string( + "There was an error checking if the member is connected to the pool." + ); + } + + try { + await testEnv.wrapperSuperToken.getPoolAdjustmentFlowInfo({ + pool: "", + providerOrSigner: testEnv.alice, + }); + } catch (err) { + expect(err.type).to.equal("GDAV1_READ"); + expect(err.message).to.have.string( + "There was an error getting the GDA pool adjustment flow information." + ); + } + }); + + it("Should throw when trying to createPool with bad params", async () => { + try { + await shouldCreatePool( + testEnv.wrapperSuperToken, + testEnv.alice, + "" + ); + } catch (err) { + expect(err.type).to.equal("GDAV1_WRITE"); + expect(err.message).to.have.string( + "There was an error creating the GDA pool." + ); + } + }); + + it("Should throw an error on SuperfluidPool view functions when wrong params passed", async () => { + const pool = await shouldCreatePool( + testEnv.wrapperSuperToken, + testEnv.alice, + testEnv.alice.address + ); + + try { + await pool.getPoolAdmin("" as any); + } catch (err) { + expect(err.type).to.equal("SUPERFLUID_POOL_READ"); + expect(err.message).to.have.string( + "There was an error getting the pool admin." + ); + } + + try { + await pool.getSuperToken("" as any); + } catch (err) { + expect(err.type).to.equal("SUPERFLUID_POOL_READ"); + expect(err.message).to.have.string( + "There was an error getting the pool's SuperToken." + ); + } + + try { + await pool.getTotalUnits("" as any); + } catch (err) { + expect(err.type).to.equal("SUPERFLUID_POOL_READ"); + expect(err.message).to.have.string( + "There was an error getting total units." + ); + } + + try { + await pool.getTotalConnectedUnits("" as any); + } catch (err) { + expect(err.type).to.equal("SUPERFLUID_POOL_READ"); + expect(err.message).to.have.string( + "There was an error getting total connected units." + ); + } + + try { + await pool.getUnits({} as any); + } catch (err) { + expect(err.type).to.equal("SUPERFLUID_POOL_READ"); + expect(err.message).to.have.string( + "There was an error getting units." + ); + } + + try { + await pool.getTotalConnectedFlowRate("" as any); + } catch (err) { + expect(err.type).to.equal("SUPERFLUID_POOL_READ"); + expect(err.message).to.have.string( + "There was an error getting total connected flow rate." + ); + } + + try { + await pool.getTotalDisconnectedFlowRate("" as any); + } catch (err) { + expect(err.type).to.equal("SUPERFLUID_POOL_READ"); + expect(err.message).to.have.string( + "There was an error getting total disconnected flow rate." + ); + } + + try { + await pool.getDisconnectedBalance({} as any); + } catch (err) { + expect(err.type).to.equal("SUPERFLUID_POOL_READ"); + expect(err.message).to.have.string( + "There was an error getting disconnected balance." + ); + } + + try { + await pool.getMemberFlowRate({} as any); + } catch (err) { + expect(err.type).to.equal("SUPERFLUID_POOL_READ"); + expect(err.message).to.have.string( + "There was an error getting member flow rate." + ); + } + + try { + await pool.getClaimable({} as any); + } catch (err) { + expect(err.type).to.equal("SUPERFLUID_POOL_READ"); + expect(err.message).to.have.string( + "There was an error getting claimable amount." + ); + } + + try { + await pool.getClaimableNow({} as any); + } catch (err) { + expect(err.type).to.equal("SUPERFLUID_POOL_READ"); + expect(err.message).to.have.string( + "There was an error getting claimable amount." + ); + } + + try { + await pool.totalSupply({} as any); + } catch (err) { + expect(err.type).to.equal("SUPERFLUID_POOL_READ"); + expect(err.message).to.have.string( + "There was an error getting total supply." + ); + } + + try { + await pool.balanceOf({} as any); + } catch (err) { + expect(err.type).to.equal("SUPERFLUID_POOL_READ"); + expect(err.message).to.have.string( + "There was an error getting balance." + ); + } + + try { + await pool.allowance({} as any); + } catch (err) { + expect(err.type).to.equal("SUPERFLUID_POOL_READ"); + expect(err.message).to.have.string( + "There was an error getting allowance." + ); + } + }); + }); + + describe("Happy Path Tests", () => { + it("Should be able to create pool", async () => { + await shouldCreatePool( + testEnv.wrapperSuperToken, + testEnv.alice, + testEnv.alice.address + ); + }); + + it("Should be able to update units for member", async () => { + const pool = await shouldCreatePool( + testEnv.wrapperSuperToken, + testEnv.alice, + testEnv.alice.address + ); + const newUnits = "10000"; + await shouldUpdateMember({ + pool, + admin: testEnv.alice, + member: testEnv.bob, + newUnits, + }); + }); + + it("Should be able to approve units", async () => { + const pool = await shouldCreatePool( + testEnv.wrapperSuperToken, + testEnv.alice, + testEnv.alice.address + ); + const newUnits = "10000"; + await pool.updateMemberUnits({ + member: testEnv.bob.address, + newUnits, + signer: testEnv.alice, + }); + const approvedUnits = "420"; + await pool.approve({ + spender: testEnv.charlie.address, + amount: approvedUnits, + signer: testEnv.bob, + }); + expect( + await pool.allowance({ + owner: testEnv.bob.address, + spender: testEnv.charlie.address, + providerOrSigner: testEnv.bob, + }) + ).to.equal(approvedUnits); + }); + + it("Should be able to increase allowance", async () => { + const pool = await shouldCreatePool( + testEnv.wrapperSuperToken, + testEnv.alice, + testEnv.alice.address + ); + const newUnits = "10000"; + await pool.updateMemberUnits({ + member: testEnv.bob.address, + newUnits, + signer: testEnv.alice, + }); + const approvedUnits = "420"; + await pool.approve({ + spender: testEnv.charlie.address, + amount: approvedUnits, + signer: testEnv.bob, + }); + const increasedUnits = "69"; + await pool.increaseAllowance({ + spender: testEnv.charlie.address, + amount: increasedUnits, + signer: testEnv.bob, + }); + expect( + await pool.allowance({ + owner: testEnv.bob.address, + spender: testEnv.charlie.address, + providerOrSigner: testEnv.bob, + }) + ).to.equal(toBN(approvedUnits).add(toBN(increasedUnits))); + }); + + it("Should be able to decrease allowance", async () => { + const pool = await shouldCreatePool( + testEnv.wrapperSuperToken, + testEnv.alice, + testEnv.alice.address + ); + const newUnits = "10000"; + await pool.updateMemberUnits({ + member: testEnv.bob.address, + newUnits, + signer: testEnv.alice, + }); + const approvedUnits = "420"; + await pool.approve({ + spender: testEnv.charlie.address, + amount: approvedUnits, + signer: testEnv.bob, + }); + const decreasedUnits = "69"; + await pool.decreaseAllowance({ + spender: testEnv.charlie.address, + amount: decreasedUnits, + signer: testEnv.bob, + }); + expect( + await pool.allowance({ + owner: testEnv.bob.address, + spender: testEnv.charlie.address, + providerOrSigner: testEnv.bob, + }) + ).to.equal(toBN(approvedUnits).sub(toBN(decreasedUnits))); + }); + + it("Should be able to transfer units", async () => { + const pool = await shouldCreatePool( + testEnv.wrapperSuperToken, + testEnv.alice, + testEnv.alice.address + ); + const newUnits = "10000"; + await pool.updateMemberUnits({ + member: testEnv.bob.address, + newUnits, + signer: testEnv.alice, + }); + const transferUnits = "420"; + await pool.transfer({ + to: testEnv.charlie.address, + amount: transferUnits, + signer: testEnv.bob, + }); + expect( + await pool.balanceOf({ + account: testEnv.charlie.address, + providerOrSigner: testEnv.charlie, + }) + ).to.equal(transferUnits); + expect( + await pool.balanceOf({ + account: testEnv.bob.address, + providerOrSigner: testEnv.bob, + }) + ).to.equal(toBN(newUnits).sub(toBN(transferUnits))); + }); + + it("Should be able to transferFrom units", async () => { + const pool = await shouldCreatePool( + testEnv.wrapperSuperToken, + testEnv.alice, + testEnv.alice.address + ); + const newUnits = "10000"; + await pool.updateMemberUnits({ + member: testEnv.bob.address, + newUnits, + signer: testEnv.alice, + }); + const approvedUnits = "420"; + await pool.approve({ + spender: testEnv.charlie.address, + amount: approvedUnits, + signer: testEnv.bob, + }); + const transferUnits = "69"; + await pool.transferFrom({ + from: testEnv.bob.address, + to: testEnv.charlie.address, + amount: transferUnits, + signer: testEnv.charlie, + }); + expect( + await pool.balanceOf({ + account: testEnv.charlie.address, + providerOrSigner: testEnv.charlie, + }) + ).to.equal(transferUnits); + expect( + await pool.balanceOf({ + account: testEnv.bob.address, + providerOrSigner: testEnv.bob, + }) + ).to.equal(toBN(newUnits).sub(toBN(transferUnits))); + }); + + it("Should be able to update units (increase)", async () => { + const pool = await shouldCreatePool( + testEnv.wrapperSuperToken, + testEnv.alice, + testEnv.alice.address + ); + const newUnits = "69"; + await pool.updateMemberUnits({ + member: testEnv.bob.address, + newUnits, + signer: testEnv.alice, + }); + const increasedUnits = "420"; + await pool.updateMemberUnits({ + member: testEnv.bob.address, + newUnits: increasedUnits, + signer: testEnv.alice, + }); + expect( + await pool.balanceOf({ + account: testEnv.bob.address, + providerOrSigner: testEnv.bob, + }) + ).to.equal(increasedUnits); + }); + + it("Should be able to update units (decrease)", async () => { + const pool = await shouldCreatePool( + testEnv.wrapperSuperToken, + testEnv.alice, + testEnv.alice.address + ); + const newUnits = "420"; + await pool.updateMemberUnits({ + member: testEnv.bob.address, + newUnits, + signer: testEnv.alice, + }); + const decreasedUnits = "69"; + await pool.updateMemberUnits({ + member: testEnv.bob.address, + newUnits: decreasedUnits, + signer: testEnv.alice, + }); + expect( + await pool.balanceOf({ + account: testEnv.bob.address, + providerOrSigner: testEnv.bob, + }) + ).to.equal(decreasedUnits); + }); + + it("Should be able to update units (remove all)", async () => { + const pool = await shouldCreatePool( + testEnv.wrapperSuperToken, + testEnv.alice, + testEnv.alice.address + ); + const newUnits = "420"; + await pool.updateMemberUnits({ + member: testEnv.bob.address, + newUnits, + signer: testEnv.alice, + }); + await pool.updateMemberUnits({ + member: testEnv.bob.address, + newUnits: "0", + signer: testEnv.alice, + }); + expect( + await pool.balanceOf({ + account: testEnv.bob.address, + providerOrSigner: testEnv.bob, + }) + ).to.equal("0"); + }); + + context( + "Should be able to connect and disconnect from pool", + async () => { + let pool: SuperfluidPool; + beforeEach(async () => { + pool = await shouldCreatePool( + testEnv.wrapperSuperToken, + testEnv.alice, + testEnv.alice.address + ); + const newUnits = "420"; + await pool.updateMemberUnits({ + member: testEnv.bob.address, + newUnits, + signer: testEnv.alice, + }); + }); + + it("With Call Agreement", async () => { + await shouldConnectPool({ + testEnv, + shouldUseCallAgreement: true, + superToken: testEnv.wrapperSuperToken, + pool, + member: testEnv.bob, + doConnect: true, + }); + await shouldConnectPool({ + testEnv, + shouldUseCallAgreement: true, + superToken: testEnv.wrapperSuperToken, + pool, + member: testEnv.bob, + doConnect: false, + }); + }); + + it("With Forwarder", async () => { + await shouldConnectPool({ + testEnv, + shouldUseCallAgreement: false, + superToken: testEnv.wrapperSuperToken, + pool, + member: testEnv.bob, + doConnect: true, + }); + await shouldConnectPool({ + testEnv, + shouldUseCallAgreement: false, + superToken: testEnv.wrapperSuperToken, + pool, + member: testEnv.bob, + doConnect: false, + }); + }); + } + ); + + context("Should be able to distribute tokens", async () => { + it("With Call Agreement", async () => { + await shouldInstantDistributeTokensToOneMember({ + testEnv, + newUnits: "10", + amountToDistribute: "1000", + admin: testEnv.alice, + distributor: testEnv.alice, + member: testEnv.bob, + shouldUseCallAgreement: true, + }); + }); + + it("With Forwarder", async () => { + await shouldInstantDistributeTokensToOneMember({ + testEnv, + newUnits: "10", + amountToDistribute: "1000", + admin: testEnv.alice, + distributor: testEnv.alice, + member: testEnv.bob, + shouldUseCallAgreement: false, + }); + }); + }); + + it("Should be able to distribute flow tokens", async () => { + it("With Call Agreement", async () => { + await shouldDistributeFlow({ + testEnv, + superToken: testEnv.wrapperSuperToken, + newUnits: "10", + requestedFlowRate: "1000", + admin: testEnv.alice, + distributor: testEnv.alice, + member: testEnv.bob, + shouldUseCallAgreement: true, + }); + }); + + it("With Forwarder", async () => { + await shouldDistributeFlow({ + testEnv, + superToken: testEnv.wrapperSuperToken, + newUnits: "10", + requestedFlowRate: "1000", + admin: testEnv.alice, + distributor: testEnv.alice, + member: testEnv.bob, + shouldUseCallAgreement: false, + }); + }); + }); + + it("Should be able to claimAllForMember as the member", async () => { + const pool = await shouldInstantDistributeTokensToOneMember({ + testEnv, + shouldUseCallAgreement: true, + newUnits: "1000", + amountToDistribute: "100000", + admin: testEnv.alice, + distributor: testEnv.alice, + member: testEnv.bob, + }); + + await shouldClaimAllForMember({ + pool, + member: testEnv.bob, + claimer: testEnv.bob, + superToken: testEnv.wrapperSuperToken, + }); + }); + + it("Should be able to claimAllForMember for someone else (alice claims for bob)", async () => { + const pool = await shouldInstantDistributeTokensToOneMember({ + testEnv, + shouldUseCallAgreement: true, + newUnits: "1000", + amountToDistribute: "100000", + admin: testEnv.alice, + distributor: testEnv.alice, + member: testEnv.bob, + }); + await shouldClaimAllForMember({ + pool, + member: testEnv.bob, + claimer: testEnv.alice, + superToken: testEnv.wrapperSuperToken, + }); + }); + + it("Should be able to claimAll", async () => { + const pool = await shouldInstantDistributeTokensToOneMember({ + testEnv, + shouldUseCallAgreement: true, + newUnits: "1000", + amountToDistribute: "100000", + admin: testEnv.alice, + distributor: testEnv.alice, + member: testEnv.bob, + }); + await shouldClaimAllForMember({ + pool, + member: testEnv.bob, + claimer: testEnv.bob, + superToken: testEnv.wrapperSuperToken, + claimAll: true, + }); + }); + }); + } +); + +const shouldCreatePool = async ( + superToken: WrapperSuperToken, + signer: SignerWithAddress, + admin: string +) => { + const data = await superToken.createPool({ + admin, + config: { + transferabilityForUnitsOwner: true, + distributionFromAnyAddress: true, + }, + signer, + }); + + expect( + await superToken.isPool({ + account: data.pool.contract.address, + providerOrSigner: signer, + }) + ).to.be.true; + const pool = new SuperfluidPool(data.pool.contract.address); + + expect(await pool.getPoolAdmin(signer)).to.be.equal(signer.address); + expect(await pool.getSuperToken(signer)).to.be.equal(superToken.address); + + return pool; +}; + +const shouldUpdateMember = async (params: ShouldUpdateMemberParams) => { + const providerSigner = params.member; + const memberUnitsBefore = await params.pool.getUnits({ + member: params.member.address, + providerOrSigner: providerSigner, + }); + const balanceOfBefore = await params.pool.balanceOf({ + account: params.member.address, + providerOrSigner: providerSigner, + }); + const totalSupplyBefore = await params.pool.totalSupply(providerSigner); + const totalUnitsBefore = await params.pool.getTotalUnits(providerSigner); + const unitsDelta = toBN(params.newUnits).sub(memberUnitsBefore); + await params.pool.updateMemberUnits({ + member: params.member.address, + newUnits: params.newUnits, + signer: params.admin, + }); + + // assert total balance/total supply + expect(await params.pool.totalSupply(providerSigner)).to.equal( + toBN(totalSupplyBefore).add(unitsDelta) + ); + expect(await params.pool.getTotalUnits(providerSigner)).to.equal( + toBN(totalUnitsBefore).add(unitsDelta) + ); + + // assert member's balance/units + expect( + await params.pool.balanceOf({ + account: params.member.address, + providerOrSigner: providerSigner, + }) + ).to.equal(toBN(balanceOfBefore).add(unitsDelta)); + expect( + await params.pool.getUnits({ + member: params.member.address, + providerOrSigner: providerSigner, + }) + ).to.equal(toBN(memberUnitsBefore).add(unitsDelta)); +}; + +const shouldConnectPool = async (params: ShouldConnectPoolParams) => { + const connectPoolOperation = params.doConnect + ? await params.superToken.connectPool({ + pool: params.pool.contract.address, + shouldUseCallAgreement: params.shouldUseCallAgreement, + }) + : await params.superToken.disconnectPool({ + pool: params.pool.contract.address, + shouldUseCallAgreement: params.shouldUseCallAgreement, + }); + await connectPoolOperation.exec(params.member); + + await validateOperationShouldUseCallAgreement( + params.testEnv, + connectPoolOperation, + params.shouldUseCallAgreement, + params.testEnv.sdkFramework.gdaV1.forwarder.address + ); + + expect( + await params.superToken.isMemberConnected({ + pool: params.pool.contract.address, + member: params.member.address, + providerOrSigner: params.member, + }) + ).to.equal(params.doConnect); + + if (params.doConnect) { + expect( + await params.pool.getTotalConnectedUnits(params.member) + ).to.equal( + await params.pool.getUnits({ + member: params.member.address, + providerOrSigner: params.member, + }) + ); + } else { + expect( + await params.pool.getTotalConnectedUnits(params.member) + ).to.equal("0"); + } +}; + +const shouldInstantDistributeTokensToOneMember = async ( + params: ShouldInstantDistributeParams +) => { + const pool = await shouldCreatePool( + params.testEnv.wrapperSuperToken, + params.admin, + params.admin.address + ); + const distributorBalanceBefore = + await params.testEnv.wrapperSuperToken.balanceOf({ + account: params.distributor.address, + providerOrSigner: params.distributor, + }); + const memberBalanceBefore = + await params.testEnv.wrapperSuperToken.balanceOf({ + account: params.member.address, + providerOrSigner: params.member, + }); + await pool.updateMemberUnits({ + member: params.member.address, + newUnits: params.newUnits, + signer: params.admin, + }); + const actualAmountDistributed = + await params.testEnv.wrapperSuperToken.estimateDistributionActualAmount( + { + from: params.distributor.address, + requestedAmount: params.amountToDistribute, + pool: pool.contract.address, + providerOrSigner: params.distributor, + } + ); + const operation = await params.testEnv.wrapperSuperToken.distributeWithGDA({ + from: params.distributor.address, + requestedAmount: params.amountToDistribute, + pool: pool.contract.address, + shouldUseCallAgreement: params.shouldUseCallAgreement, + }); + await operation.exec(params.distributor); + await validateOperationShouldUseCallAgreement( + params.testEnv, + operation, + params.shouldUseCallAgreement, + params.testEnv.sdkFramework.gdaV1.forwarder.address + ); + + const distributorBalanceAfter = + await params.testEnv.wrapperSuperToken.balanceOf({ + account: params.distributor.address, + providerOrSigner: params.distributor, + }); + const memberBalanceAfter = await params.testEnv.wrapperSuperToken.balanceOf( + { + account: params.member.address, + providerOrSigner: params.member, + } + ); + expect(distributorBalanceAfter).to.equal( + toBN(distributorBalanceBefore).sub(toBN(actualAmountDistributed)) + ); + + const isMemberConnected = + await params.testEnv.wrapperSuperToken.isMemberConnected({ + pool: pool.contract.address, + member: params.member.address, + providerOrSigner: params.member, + }); + if (isMemberConnected) { + expect(memberBalanceAfter).to.equal( + toBN(memberBalanceBefore).add(toBN(actualAmountDistributed)) + ); + } else { + expect(memberBalanceAfter).to.equal(memberBalanceBefore); + } + + return pool; +}; + +const shouldDistributeFlow = async (params: ShouldFlowDistributeParams) => { + const pool = await shouldCreatePool( + params.superToken, + params.admin, + params.admin.address + ); + const newUnits = "10"; + await pool.updateMemberUnits({ + member: params.member.address, + newUnits, + signer: params.admin, + }); + const providerOrSigner = params.admin; + const requestedFlowRate = "1000"; + const actualDistributionFlowRate = + await params.superToken.estimateFlowDistributionActualFlowRate({ + from: params.distributor.address, + requestedFlowRate: requestedFlowRate, + pool: pool.contract.address, + providerOrSigner, + }); + const operation = await params.superToken.distributeFlow({ + from: params.distributor.address, + requestedFlowRate: requestedFlowRate, + pool: pool.contract.address, + shouldUseCallAgreement: true, + }); + await operation.exec(params.distributor); + + await validateOperationShouldUseCallAgreement( + params.testEnv, + operation, + params.shouldUseCallAgreement, + params.testEnv.sdkFramework.gdaV1.forwarder.address + ); + + expect( + await params.superToken.getGDANetFlow({ + account: params.distributor.address, + providerOrSigner, + }) + ).to.equal(toBN(actualDistributionFlowRate.actualFlowRate).mul(toBN("-1"))); + + const connectPoolOperation = await params.superToken.connectPool({ + pool: pool.contract.address, + shouldUseCallAgreement: true, + }); + await connectPoolOperation.exec(params.member); + + expect( + await params.superToken.getGDANetFlow({ + account: params.member.address, + providerOrSigner, + }) + ).to.equal(toBN(actualDistributionFlowRate.actualFlowRate)); + + expect( + await params.superToken.getPoolAdjustmentFlowRate({ + pool: pool.contract.address, + providerOrSigner, + }) + ).to.equal("0"); + + const poolAdjustmentFlowInfo = + await params.superToken.getPoolAdjustmentFlowInfo({ + pool: pool.contract.address, + providerOrSigner, + }); + expect(poolAdjustmentFlowInfo.flowRate).to.equal("0"); + expect(poolAdjustmentFlowInfo.recipient).to.equal(params.admin.address); + const encoder = new ethers.utils.AbiCoder(); + const network = await providerOrSigner.provider?.getNetwork(); + if (!network) throw new Error("no network"); + + const encodedData = encoder.encode( + ["uint256", "string", "address", "address"], + [ + network.chainId, + "poolAdjustmentFlow", + pool.contract.address, + params.admin.address, + ] + ); + const flowHash = ethers.utils.keccak256(encodedData); + expect(poolAdjustmentFlowInfo.flowHash).to.equal(flowHash); +}; + +const shouldClaimAllForMember = async ( + params: ShouldClaimAllForMemberParams +) => { + const memberBalanceBefore = await params.superToken.balanceOf({ + account: params.member.address, + providerOrSigner: params.member, + }); + const claimableBalanceData = await params.pool.getClaimableNow({ + member: params.member.address, + providerOrSigner: params.member, + }); + + if (params.claimAll) { + await params.pool.claimAll(params.member); + } else { + await params.pool.claimAllForMember({ + member: params.member.address, + signer: params.claimer, + }); + } + + const memberBalanceAfter = await params.superToken.balanceOf({ + account: params.member.address, + providerOrSigner: params.member, + }); + expect(toBN(memberBalanceAfter).sub(toBN(memberBalanceBefore))).to.equal( + claimableBalanceData.claimableBalance + ); +}; diff --git a/packages/sdk-core/test/4_governance.test.ts b/packages/sdk-core/test/4_governance.test.ts index b807295c90..38e0f6e5bb 100644 --- a/packages/sdk-core/test/4_governance.test.ts +++ b/packages/sdk-core/test/4_governance.test.ts @@ -13,9 +13,11 @@ makeSuite("Governance Tests", (testEnv: TestEnvironment) => { expect(defaultParams.patricianPeriod).to.equal( testEnv.constants.PATRICIAN_PERIOD ); - expect(defaultParams.rewardAddress).to.equal( - testEnv.constants.DEFAULT_REWARD_ADDRESS - ); + const defaultRewardAddress = + await testEnv.sdkFramework.governance.getRewardAddress({ + providerOrSigner: testEnv.alice, + }); + expect(defaultParams.rewardAddress).to.equal(defaultRewardAddress); expect(defaultParams.minimumDeposit).to.equal("0"); }); @@ -31,8 +33,12 @@ makeSuite("Governance Tests", (testEnv: TestEnvironment) => { expect(tokenSpecificParams.patricianPeriod).to.equal( testEnv.constants.PATRICIAN_PERIOD ); + const defaultRewardAddress = + await testEnv.sdkFramework.governance.getRewardAddress({ + providerOrSigner: testEnv.alice, + }); expect(tokenSpecificParams.rewardAddress).to.equal( - testEnv.constants.DEFAULT_REWARD_ADDRESS + defaultRewardAddress ); expect(tokenSpecificParams.minimumDeposit).to.equal("0"); }); diff --git a/packages/sdk-core/test/TestEnvironment.ts b/packages/sdk-core/test/TestEnvironment.ts index a395442059..e0c2e17f73 100644 --- a/packages/sdk-core/test/TestEnvironment.ts +++ b/packages/sdk-core/test/TestEnvironment.ts @@ -1,8 +1,10 @@ import hre, { ethers } from "hardhat"; import { IConstantFlowAgreementV1, + IGeneralDistributionAgreementV1, IInstantDistributionAgreementV1, SuperfluidFrameworkDeployer, + SuperfluidFrameworkDeploymentSteps, TestToken, TestToken__factory, } from "../src/typechain-types"; @@ -34,7 +36,7 @@ export interface TestEnvironment { provider: JsonRpcProvider; sdkFramework: Framework; superfluidFrameworkDeployer: SuperfluidFrameworkDeployer; - frameworkAddresses: SuperfluidFrameworkDeployer.FrameworkStructOutput; + frameworkAddresses: SuperfluidFrameworkDeploymentSteps.FrameworkStructOutput; constants: typeof TEST_ENVIRONMENT_CONSTANTS; users: SignerWithAddress[]; alice: SignerWithAddress; @@ -42,6 +44,7 @@ export interface TestEnvironment { charlie: SignerWithAddress; cfaV1: IConstantFlowAgreementV1; idaV1: IInstantDistributionAgreementV1; + gdaV1: IGeneralDistributionAgreementV1; wrapperSuperToken: WrapperSuperToken; nativeAssetSuperToken: NativeAssetSuperToken; pureSuperToken: PureSuperToken; @@ -53,7 +56,8 @@ const testEnv: TestEnvironment = { provider: hre.ethers.provider, sdkFramework: {} as Framework, superfluidFrameworkDeployer: {} as SuperfluidFrameworkDeployer, - frameworkAddresses: {} as SuperfluidFrameworkDeployer.FrameworkStructOutput, + frameworkAddresses: + {} as SuperfluidFrameworkDeploymentSteps.FrameworkStructOutput, constants: TEST_ENVIRONMENT_CONSTANTS, alice: {} as SignerWithAddress, bob: {} as SignerWithAddress, @@ -61,6 +65,7 @@ const testEnv: TestEnvironment = { users: [], cfaV1: {} as IConstantFlowAgreementV1, idaV1: {} as IInstantDistributionAgreementV1, + gdaV1: {} as IGeneralDistributionAgreementV1, token: {} as TestToken, wrapperSuperToken: {} as WrapperSuperToken, nativeAssetSuperToken: {} as NativeAssetSuperToken, @@ -96,6 +101,7 @@ export const initializeTestEnvironment = async () => { console.log("Set Agreement Contracts..."); testEnv.cfaV1 = testEnv.sdkFramework.cfaV1.contract.connect(testEnv.alice); testEnv.idaV1 = testEnv.sdkFramework.idaV1.contract.connect(testEnv.alice); + testEnv.gdaV1 = testEnv.sdkFramework.gdaV1.contract.connect(testEnv.alice); console.log("Load SuperToken and TestToken..."); testEnv.wrapperSuperToken = diff --git a/packages/solidity-semantic-money/src/ref-impl/ISuperfluidPool.sol b/packages/solidity-semantic-money/src/ref-impl/ISuperfluidPool.sol index cf1c1a4074..86ffe6cc33 100644 --- a/packages/solidity-semantic-money/src/ref-impl/ISuperfluidPool.sol +++ b/packages/solidity-semantic-money/src/ref-impl/ISuperfluidPool.sol @@ -67,4 +67,4 @@ interface ISuperfluidPoolOperator { /// Settle the claim function poolSettleClaim(address claimRecipient, Value amount) external returns (bool); -} +} \ No newline at end of file diff --git a/packages/subgraph/.prettierrc.js b/packages/subgraph/.prettierrc.js index bc6a4ec169..11e1937436 100644 --- a/packages/subgraph/.prettierrc.js +++ b/packages/subgraph/.prettierrc.js @@ -2,4 +2,5 @@ module.exports = { trailingComma: "es5", singleQuote: false, bracketSpacing: true, + printWidth: 120 }; diff --git a/packages/subgraph/config/mock.json b/packages/subgraph/config/mock.json index 2828806c15..0a91e0f421 100644 --- a/packages/subgraph/config/mock.json +++ b/packages/subgraph/config/mock.json @@ -4,6 +4,7 @@ "hostAddress": "0x0000000000000000000000000000000000000000", "cfaAddress": "0x0000000000000000000000000000000000000000", "idaAddress": "0x0000000000000000000000000000000000000000", + "gdaAddress": "0x0000000000000000000000000000000000000000", "superTokenFactoryAddress": "0x0000000000000000000000000000000000000000", "resolverV1Address": "0x0000000000000000000000000000000000000000", "nativeAssetSuperTokenAddress": "0x0000000000000000000000000000000000000000", diff --git a/packages/subgraph/package.json b/packages/subgraph/package.json index 9a9c89f0dd..ff8c4b0dc9 100644 --- a/packages/subgraph/package.json +++ b/packages/subgraph/package.json @@ -23,7 +23,7 @@ "matchstick:prepare-addresses": "mustache config/polygon-mainnet.json src/addresses.template.ts > src/addresses.ts", "matchstick:prepare-generated": "yarn getAbi && yarn codegen && yarn generate-sf-meta-local", "matchstick:test": "graph test", - "dev": "nodemon -e ts -x yarn matchstick:test", + "dev": "yarn matchstick && nodemon -e ts -x yarn matchstick:test", "posttest": "yarn testenv:stop", "integrity": "npx hardhat run scripts/dataIntegrity/dataIntegrityTest.ts --network", "check-indexing-completeness": "ts-node scripts/checkIsDeployedOnAllNetworks.ts", @@ -33,6 +33,7 @@ "remove-local": "graph remove superfluid-test --node http://localhost:8020/", "deploy-local": "graph deploy superfluid-test --node http://localhost:8020/ --ipfs http://localhost:5001 --version-label v1.0.0", "prepare-local": "run-s prepare-local:*", + "prepare-local:deploy-contracts": "cd ../ethereum-contracts && npx hardhat run dev-scripts/run-deploy-contracts-and-token.js && cd ../subgraph", "prepare-local:manifest": "yarn prepare-manifest-local", "prepare-local:network": "yarn set-network-local", "prepare-local:abi": "yarn getAbi", @@ -51,7 +52,7 @@ "dependencies": { "@graphprotocol/graph-cli": "0.57.0", "@graphprotocol/graph-ts": "0.31.0", - "@superfluid-finance/sdk-core": "0.6.8", + "@superfluid-finance/sdk-core": "0.6.13", "mustache": "^4.2.0" }, "devDependencies": { diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index 7074712f4c..da309b0a93 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -613,6 +613,212 @@ type SubscriptionUnitsUpdatedEvent implements Event @entity(immutable: true) { subscription: IndexSubscription! } +# GeneralDistributionAgreementV1 # + +type PoolCreatedEvent implements Event @entity(immutable: true) { + id: ID! + transactionHash: Bytes! + gasPrice: BigInt! + gasUsed: BigInt! + timestamp: BigInt! + name: String! + + """ + Contains the addresses that were impacted by this event: + addresses[0] = `token` (superToken) + addresses[1] = `pool` + addresses[2] = `caller` + addresses[3] = `admin` + """ + addresses: [Bytes!]! + blockNumber: BigInt! + logIndex: BigInt! + order: BigInt! + + token: Bytes! + caller: Bytes! + admin: Bytes! + + pool: Pool! +} + +type PoolConnectionUpdatedEvent implements Event @entity(immutable: true) { + id: ID! + transactionHash: Bytes! + gasPrice: BigInt! + gasUsed: BigInt! + timestamp: BigInt! + name: String! + + """ + Contains the addresses that were impacted by this event: + addresses[0] = `token` (superToken) + addresses[1] = `pool` + addresses[2] = `poolMember` + """ + addresses: [Bytes!]! + blockNumber: BigInt! + logIndex: BigInt! + order: BigInt! + + token: Bytes! + connected: Boolean! + userData: Bytes! + + pool: Pool! + poolMember: PoolMember! +} + +type BufferAdjustedEvent implements Event @entity(immutable: true) { + id: ID! + transactionHash: Bytes! + gasPrice: BigInt! + gasUsed: BigInt! + timestamp: BigInt! + name: String! + + """ + Contains the addresses that were impacted by this event: + addresses[0] = `token` (superToken) + addresses[1] = `pool` + addresses[2] = `distributor` + addresses[3] = `operator` + """ + addresses: [Bytes!]! + blockNumber: BigInt! + logIndex: BigInt! + order: BigInt! + + token: Bytes! + bufferDelta: BigInt! + newBufferAmount: BigInt! + totalBufferAmount: BigInt! + + pool: Pool! + poolDistributor: PoolDistributor! +} + +type InstantDistributionUpdatedEvent implements Event @entity(immutable: true) { + id: ID! + transactionHash: Bytes! + gasPrice: BigInt! + gasUsed: BigInt! + timestamp: BigInt! + name: String! + + """ + Contains the addresses that were impacted by this event: + addresses[0] = `token` (superToken) + addresses[1] = `pool` + addresses[2] = `poolDistributor` + addresses[3] = `operator` + """ + addresses: [Bytes!]! + blockNumber: BigInt! + logIndex: BigInt! + order: BigInt! + + token: Bytes! + operator: Bytes! + requestedAmount: BigInt! + actualAmount: BigInt! + totalUnits: BigInt! + userData: Bytes! + + pool: Pool! + poolDistributor: PoolDistributor! +} + +type FlowDistributionUpdatedEvent implements Event @entity(immutable: true) { + id: ID! + transactionHash: Bytes! + gasPrice: BigInt! + gasUsed: BigInt! + timestamp: BigInt! + name: String! + + """ + Contains the addresses that were impacted by this event: + addresses[0] = `token` (superToken) + addresses[1] = `pool` + addresses[2] = `poolDistributor` + addresses[3] = `operator` + """ + addresses: [Bytes!]! + blockNumber: BigInt! + logIndex: BigInt! + order: BigInt! + + token: Bytes! + operator: Bytes! + oldFlowRate: BigInt! + newDistributorToPoolFlowRate: BigInt! + newTotalDistributionFlowRate: BigInt! + adjustmentFlowRecipient: Bytes! + adjustmentFlowRate: BigInt! + totalUnits: BigInt! + userData: Bytes! + + pool: Pool! + poolDistributor: PoolDistributor! +} + +# SuperfluidPool # +type DistributionClaimedEvent implements Event @entity(immutable: true) { + id: ID! + transactionHash: Bytes! + gasPrice: BigInt! + gasUsed: BigInt! + timestamp: BigInt! + name: String! + + """ + Contains the addresses that were impacted by this event: + addresses[0] = `token` (superToken) + addresses[1] = `pool` + addresses[2] = `member` + """ + addresses: [Bytes!]! + blockNumber: BigInt! + logIndex: BigInt! + order: BigInt! + + token: Bytes! + claimedAmount: BigInt! + totalClaimed: BigInt! + + pool: Pool! + poolMember: PoolMember! +} + +type MemberUnitsUpdatedEvent implements Event @entity(immutable: true) { + id: ID! + transactionHash: Bytes! + gasPrice: BigInt! + gasUsed: BigInt! + timestamp: BigInt! + name: String! + + """ + Contains the addresses that were impacted by this event: + addresses[0] = `token` (superToken) + addresses[1] = `pool` + addresses[2] = `member` + """ + addresses: [Bytes!]! + blockNumber: BigInt! + logIndex: BigInt! + order: BigInt! + + token: Bytes! + oldUnits: BigInt! + units: BigInt! + totalUnits: BigInt! + + pool: Pool! + poolMember: PoolMember! +} + # Host # type AgreementClassRegisteredEvent implements Event @entity(immutable: true) { @@ -943,7 +1149,8 @@ type PPPConfigurationChangedEvent implements Event @entity(immutable: true) { patricianPeriod: BigInt! } -type SuperTokenMinimumDepositChangedEvent implements Event @entity(immutable: true) { +type SuperTokenMinimumDepositChangedEvent implements Event + @entity(immutable: true) { id: ID! transactionHash: Bytes! gasPrice: BigInt! @@ -1520,6 +1727,9 @@ type Account @entity { subscriptions: [IndexSubscription!]! @derivedFrom(field: "subscriber") publishedIndexes: [Index!]! @derivedFrom(field: "publisher") + pools: [Pool!]! @derivedFrom(field: "admin") + poolMemberships: [PoolMember!]! @derivedFrom(field: "account") + sentTransferEvents: [TransferEvent!]! @derivedFrom(field: "from") receivedTransferEvents: [TransferEvent!]! @derivedFrom(field: "to") @@ -1531,6 +1741,115 @@ type Account @entity { @derivedFrom(field: "account") } +type Pool @entity { + """ + ID: poolAddress + """ + id: ID! + createdAtTimestamp: BigInt! + createdAtBlockNumber: BigInt! + updatedAtTimestamp: BigInt! + updatedAtBlockNumber: BigInt! + + totalUnits: BigInt! + totalConnectedUnits: BigInt! + totalDisconnectedUnits: BigInt! + totalAmountInstantlyDistributedUntilUpdatedAt: BigInt! + totalAmountFlowedDistributedUntilUpdatedAt: BigInt! + totalAmountDistributedUntilUpdatedAt: BigInt! + """ + A member is any account which has more than 0 units in the pool. + """ + totalMembers: Int! + """ + A connected member is any account which has more than 0 units in the pool and is connected. + """ + totalConnectedMembers: Int! + """ + A disconnected member is any account which has more than 0 units in the pool and is not connected. + """ + totalDisconnectedMembers: Int! + adjustmentFlowRate: BigInt! + flowRate: BigInt! + totalBuffer: BigInt! + token: Token! + admin: Account! + + # ---------------------------------- links ---------------------------------- + # HOL Entity Links + poolDistributors: [PoolDistributor!]! @derivedFrom(field: "pool") + poolMembers: [PoolMember!]! @derivedFrom(field: "pool") + + # Created Event Entity Link + poolCreatedEvent: PoolCreatedEvent! @derivedFrom(field: "pool") + + # Event Entity Links + poolConnectionUpdatedEvents: [PoolConnectionUpdatedEvent!]! + @derivedFrom(field: "pool") + bufferAdjustedEvents: [BufferAdjustedEvent!]! @derivedFrom(field: "pool") + instantDistributionUpdatedEvents: [InstantDistributionUpdatedEvent!]! + @derivedFrom(field: "pool") + flowDistributionUpdatedEvents: [FlowDistributionUpdatedEvent!]! + @derivedFrom(field: "pool") + memberUnitsUpdatedEvents: [MemberUnitsUpdatedEvent!]! @derivedFrom(field: "pool") + distributionClaimedEvents: [DistributionClaimedEvent!]! + @derivedFrom(field: "pool") +} + +type PoolMember @entity { + """ + ID composed of: "poolMember"-poolAddress-poolMemberAddress + """ + id: ID! + createdAtTimestamp: BigInt! + createdAtBlockNumber: BigInt! + updatedAtTimestamp: BigInt! + updatedAtBlockNumber: BigInt! + + units: BigInt! + isConnected: Boolean! + totalAmountClaimed: BigInt! + + account: Account! + pool: Pool! + + # ---------------------------------- links ---------------------------------- + poolConnectionUpdatedEvents: [PoolConnectionUpdatedEvent!]! + @derivedFrom(field: "poolMember") + memberUnitsUpdatedEvents: [MemberUnitsUpdatedEvent!]! + @derivedFrom(field: "poolMember") + distributionClaimedEvents: [DistributionClaimedEvent!]! + @derivedFrom(field: "poolMember") +} + +type PoolDistributor @entity { + """ + ID composed of: "poolDistributor"-pool-poolDistributorAddress + """ + id: ID! + createdAtTimestamp: BigInt! + createdAtBlockNumber: BigInt! + updatedAtTimestamp: BigInt! + updatedAtBlockNumber: BigInt! + + totalAmountInstantlyDistributedUntilUpdatedAt: BigInt! + totalAmountFlowedDistributedUntilUpdatedAt: BigInt! + totalAmountDistributedUntilUpdatedAt: BigInt! + totalBuffer: BigInt! + flowRate: BigInt! + + account: Account! + pool: Pool! + + # ---------------------------------- links ---------------------------------- + bufferAdjustedEvents: [BufferAdjustedEvent!]! + @derivedFrom(field: "poolDistributor") + instantDistributionUpdatedEvents: [InstantDistributionUpdatedEvent!]! + @derivedFrom(field: "poolDistributor") + flowDistributionUpdatedEvents: [FlowDistributionUpdatedEvent!]! + @derivedFrom(field: "poolDistributor") +} + """ Index: An Index higher order entity. """ @@ -1931,35 +2250,97 @@ type AccountTokenSnapshot @entity { maybeCriticalAtTimestamp: BigInt """ - The count of currently open streams for an account, both incoming and outgoing. + The count of currently open streams for an account, both incoming and outgoing for all agreements. """ totalNumberOfActiveStreams: Int! """ - The count of active outgoing streams from this account. + The count of currently open streams for an account, both incoming and outgoing for the CFA. + """ + totalCFANumberOfActiveStreams: Int! + + # delete this property + """ + The count of currently open streams for an account, both incoming and outgoing for the GDA. + """ + totalGDANumberOfActiveStreams: Int! + + """ + The count of active outgoing streams from this account for all agreements. """ activeOutgoingStreamCount: Int! """ - The count of active incoming streams to this account. + The count of active outgoing streams from this account for the CFA. + """ + activeCFAOutgoingStreamCount: Int! + + """ + The count of active outgoing streams from this account for the GDA. + """ + activeGDAOutgoingStreamCount: Int! + + """ + The count of active incoming streams to this account for all agreements. """ activeIncomingStreamCount: Int! """ - The count of closed streams by `account`, both incoming and outgoing. + The count of active incoming streams to this account for the CFA. + """ + activeCFAIncomingStreamCount: Int! + + # delete this property + """ + The count of active incoming streams to this account for the GDA. + """ + activeGDAIncomingStreamCount: Int! + + """ + The count of closed streams by `account`, both incoming and outgoing for all agreements. """ totalNumberOfClosedStreams: Int! """ - The count of closed outgoing streams by `account`. + The count of closed streams by `account`, both incoming and outgoing for the CFA. + """ + totalCFANumberOfClosedStreams: Int! + + """ + The count of closed streams by `account`, both incoming and outgoing for the GDA. + """ + totalGDANumberOfClosedStreams: Int! + + """ + The count of closed outgoing streams by `account` for all agreements. """ inactiveOutgoingStreamCount: Int! """ - The count of closed incoming streams by `account`. + The count of closed outgoing streams by `account` for the CFA. + """ + inactiveCFAOutgoingStreamCount: Int! + + """ + The count of closed outgoing streams by `account` for the GDA. + """ + inactiveGDAOutgoingStreamCount: Int! + + """ + The count of closed incoming streams by `account` for all agreements. """ inactiveIncomingStreamCount: Int! + """ + The count of closed incoming streams by `account` for the CFA. + """ + inactiveCFAIncomingStreamCount: Int! + + """ + The count of closed incoming streams by `account` for the GDA. + """ + inactiveGDAIncomingStreamCount: Int! + """ The current (as of updatedAt) number of subscriptions with units allocated to them tied to this `account`. """ @@ -1970,46 +2351,127 @@ type AccountTokenSnapshot @entity { """ totalApprovedSubscriptions: Int! + """ + The current (as of updatedAt) number of membership with units allocated to them tied to this `account`. + """ + totalMembershipsWithUnits: Int! + + """ + Counts all currently (as of updatedAt) approved membership whether or not they have units. + """ + totalConnectedMemberships: Int! + """ Balance of `account` as of `updatedAtTimestamp`/`updatedAtBlock`. """ balanceUntilUpdatedAt: BigInt! """ - The total deposit this account has held by the CFA agreement for `account` active streams. + The total deposit this account has held by all flow agreements for `account` active streams. """ totalDeposit: BigInt! """ - The total net flow rate of the `account` as of `updatedAtTimestamp`/`updatedAtBlock`. + The total deposit this account has held by the CFA agreement for `account` active streams. + """ + totalCFADeposit: BigInt! + + """ + The total deposit this account has held by the GDA agreement for `account` active streams. + """ + totalGDADeposit: BigInt! + + """ + The total net flow rate of the `account` as of `updatedAtTimestamp`/`updatedAtBlock` for all flow agreements. + This can be obtained by: `totalInflowRate - totalOutflowRate`. """ totalNetFlowRate: BigInt! """ - The total inflow rate (receive flowRate per second) of the `account`. + The total net flow rate of the `account` as of `updatedAtTimestamp`/`updatedAtBlock` for the CFA. + """ + totalCFANetFlowRate: BigInt! + + """ + The total net flow rate of the `account` as of `updatedAtTimestamp`/`updatedAtBlock` for the GDA. + """ + totalGDANetFlowRate: BigInt! + + """ + The total inflow rate (receive flowRate per second) of the `account` for all flow agreements. """ totalInflowRate: BigInt! """ - The total outflow rate (send flowrate per second) of the `account`. + The total inflow rate (receive flowRate per second) of the `account` for the CFA. + """ + totalCFAInflowRate: BigInt! + + """ + The total inflow rate (receive flowRate per second) of the `account` for the GDA. + """ + totalGDAInflowRate: BigInt! + + """ + The total outflow rate (send flowrate per second) of the `account` for all flow agreements. """ totalOutflowRate: BigInt! """ - The total amount of `token` streamed into this `account` until the `updatedAtTimestamp`/`updatedAtBlock`. + The total outflow rate (send flowrate per second) of the `account` for the CFA. + """ + totalCFAOutflowRate: BigInt! + + """ + The total outflow rate (send flowrate per second) of the `account` for the GDA. + """ + totalGDAOutflowRate: BigInt! + + """ + The total amount of `token` streamed into this `account` until the `updatedAtTimestamp`/`updatedAtBlock` for all flow agreements. """ totalAmountStreamedInUntilUpdatedAt: BigInt! """ - The total amount of `token` streamed from this `account` until the `updatedAtTimestamp`/`updatedAtBlock`. + The total amount of `token` streamed into this `account` until the `updatedAtTimestamp`/`updatedAtBlock` for the CFA. + """ + totalCFAAmountStreamedInUntilUpdatedAt: BigInt! + + """ + The total amount of `token` streamed into this `account` until the `updatedAtTimestamp`/`updatedAtBlock` for the GDA. + """ + totalGDAAmountStreamedInUntilUpdatedAt: BigInt! + + """ + The total amount of `token` streamed from this `account` until the `updatedAtTimestamp`/`updatedAtBlock` for all flow agreements. """ totalAmountStreamedOutUntilUpdatedAt: BigInt! """ - The total amount of `token` streamed through this `account` until the `updatedAtTimestamp`/`updatedAtBlock`. + The total amount of `token` streamed from this `account` until the `updatedAtTimestamp`/`updatedAtBlock` for the CFA. + """ + totalCFAAmountStreamedOutUntilUpdatedAt: BigInt! + + """ + The total amount of `token` streamed from this `account` until the `updatedAtTimestamp`/`updatedAtBlock` for the GDA. + """ + totalGDAAmountStreamedOutUntilUpdatedAt: BigInt! + + """ + The total amount of `token` streamed through this `account` until the `updatedAtTimestamp`/`updatedAtBlock` for all flow agreements. """ totalAmountStreamedUntilUpdatedAt: BigInt! + """ + The total amount of `token` streamed through this `account` until the `updatedAtTimestamp`/`updatedAtBlock` for the CFA. + """ + totalCFAAmountStreamedUntilUpdatedAt: BigInt! + + """ + The total amount of `token` streamed through this `account` until the `updatedAtTimestamp`/`updatedAtBlock` for the GDA. + """ + totalGDAAmountStreamedUntilUpdatedAt: BigInt! + """ The total amount of `token` this `account` has transferred. """ @@ -2042,35 +2504,95 @@ type AccountTokenSnapshotLog @entity { maybeCriticalAtTimestamp: BigInt """ - The current (as of timestamp) number of open streams. + The current (as of timestamp) number of open streams for all agreements. """ totalNumberOfActiveStreams: Int! """ - The count of active outgoing streams from this account. + The current (as of timestamp) number of open streams. + """ + totalCFANumberOfActiveStreams: Int! + + """ + The current (as of timestamp) number of open streams. + """ + totalGDANumberOfActiveStreams: Int! + + """ + The count of active outgoing streams from this account for all agreements. """ activeOutgoingStreamCount: Int! """ - The count of active incoming streams to this account. + The count of active outgoing streams from this account. + """ + activeCFAOutgoingStreamCount: Int! + + """ + The count of active outgoing streams from this account. + """ + activeGDAOutgoingStreamCount: Int! + + """ + The count of active incoming streams to this account for all agreements. """ activeIncomingStreamCount: Int! """ - The current (as of timestamp) count of closed streams. + The count of active incoming streams to this account for the CFA. + """ + activeCFAIncomingStreamCount: Int! + + """ + The count of active incoming streams to this account for the GDA. + """ + activeGDAIncomingStreamCount: Int! + + """ + The current (as of timestamp) count of closed streams for all agreements. """ totalNumberOfClosedStreams: Int! """ - The count of closed outgoing streams by `account`. + The current (as of timestamp) count of closed streams for the CFA. + """ + totalCFANumberOfClosedStreams: Int! + + """ + The current (as of timestamp) count of closed streams for the GDA. + """ + totalGDANumberOfClosedStreams: Int! + + """ + The count of closed outgoing streams by `account` for all agreements. """ inactiveOutgoingStreamCount: Int! """ - The count of closed incoming streams by `account`. + The count of closed outgoing streams by `account` for the CFA. + """ + inactiveCFAOutgoingStreamCount: Int! + + """ + The count of closed outgoing streams by `account` for the GDA. + """ + inactiveGDAOutgoingStreamCount: Int! + + """ + The count of closed incoming streams by `account` for all agreements. """ inactiveIncomingStreamCount: Int! + """ + The count of closed incoming streams by `account` for the CFA. + """ + inactiveCFAIncomingStreamCount: Int! + + """ + The count of closed incoming streams by `account` for the GDA. + """ + inactiveGDAIncomingStreamCount: Int! + """ The current (as of timestamp) number of subscriptions with units allocated to them tied to this `account`. """ @@ -2081,47 +2603,129 @@ type AccountTokenSnapshotLog @entity { """ totalApprovedSubscriptions: Int! + """ + The current (as of timestamp) number of membership with units allocated to them tied to this `account`. + """ + totalMembershipsWithUnits: Int! + + """ + Counts all currently (as of timestamp) connected membership whether or not they have units. + """ + totalConnectedMemberships: Int! + """ Balance of `account` as of `timestamp`/`block`. """ balance: BigInt! """ - The total (as of timestamp) deposit this account has held by the CFA agreement for `account` active streams. + The total (as of timestamp) deposit this account has held by all flow agreements for `account` active streams. """ totalDeposit: BigInt! + """ + The total (as of timestamp) deposit this account has held by the CFA agreement for `account` active streams. + """ + totalCFADeposit: BigInt! + + """ + The total (as of timestamp) deposit this account has held by the GDA agreement for `account` active streams. + """ + totalGDADeposit: BigInt! + """ The total (as of timestamp) net flow rate of the `account` as of `timestamp`/`block`. This can be obtained by: `totalInflowRate - totalOutflowRate` """ totalNetFlowRate: BigInt! + """ + The total (as of timestamp) net flow rate of the `account` as of `timestamp`/`block` for the CFA. + This can be obtained by: `totalCFAInflowRate - totalCFAOutflowRate` + """ + totalCFANetFlowRate: BigInt! + + """ + The total (as of timestamp) net flow rate of the `account` as of `timestamp`/`block` for the GDA. + This can be obtained by: `totalGDAInflowRate - totalGDAOutflowRate` + """ + totalGDANetFlowRate: BigInt! + """ The total (as of timestamp) inflow rate (receive flowRate per second) of the `account`. """ totalInflowRate: BigInt! + """ + The total (as of timestamp) inflow rate (receive flowRate per second) of the `account` for the CFA. + """ + totalCFAInflowRate: BigInt! + + """ + The total (as of timestamp) inflow rate (receive flowRate per second) of the `account` for the GDA. + """ + totalGDAInflowRate: BigInt! + """ The total (as of timestamp) outflow rate (send flowrate per second) of the `account`. """ totalOutflowRate: BigInt! + """ + The total (as of timestamp) outflow rate (send flowrate per second) of the `account` for the CFA. + """ + totalCFAOutflowRate: BigInt! + + """ + The total (as of timestamp) outflow rate (send flowrate per second) of the `account` for the GDA. + """ + totalGDAOutflowRate: BigInt! + """ The total (as of timestamp) amount of `token` streamed into this `account` until the `timestamp`/`block`. """ totalAmountStreamedIn: BigInt! + """ + The total (as of timestamp) amount of `token` streamed into this `account` until the `timestamp`/`block` for the CFA. + """ + totalCFAAmountStreamedIn: BigInt! + + """ + The total (as of timestamp) amount of `token` streamed into this `account` until the `timestamp`/`block` for the GDA. + """ + totalGDAAmountStreamedIn: BigInt! + """ The total (as of timestamp) amount of `token` streamed from this `account` until the `timestamp`/`block`. """ totalAmountStreamedOut: BigInt! + """ + The total (as of timestamp) amount of `token` streamed from this `account` until the `timestamp`/`block` for the CFA. + """ + totalCFAAmountStreamedOut: BigInt! + + """ + The total (as of timestamp) amount of `token` streamed from this `account` until the `timestamp`/`block` for the GDA. + """ + totalGDAAmountStreamedOut: BigInt! + """ The total (as of timestamp) net amount of `token` streamed through this `account` until the `timestamp`/`block`. """ totalAmountStreamed: BigInt! + """ + The total (as of timestamp) net amount of `token` streamed through this `account` until the `timestamp`/`block` for the CFA. + """ + totalCFAAmountStreamed: BigInt! + + """ + The total (as of timestamp) net amount of `token` streamed through this `account` until the `timestamp`/`block` for the GDA. + """ + totalGDAAmountStreamed: BigInt! + """ The total (as of timestamp) amount of `token` this `account` has transferred out until the `timestamp`/`block`. """ @@ -2148,11 +2752,31 @@ type TokenStatistic @entity { """ totalNumberOfActiveStreams: Int! + """ + The total number of currently active `token` streams for the CFA. + """ + totalCFANumberOfActiveStreams: Int! + + """ + The total number of currently active `token` streams for the GDA. + """ + totalGDANumberOfActiveStreams: Int! + """ The count of closed streams for `token`. """ totalNumberOfClosedStreams: Int! + """ + The count of closed streams for `token` for the CFA. + """ + totalCFANumberOfClosedStreams: Int! + + """ + The count of closed streams for `token` for the GDA. + """ + totalGDANumberOfClosedStreams: Int! + """ The total number of Indexes created with `token`. """ @@ -2174,20 +2798,70 @@ type TokenStatistic @entity { totalApprovedSubscriptions: Int! """ - The total deposit held by the CFA agreement for this particular `token`. + The total number of Pools created with `token`. + """ + totalNumberOfPools: Int! + + """ + The total number of "active" (has greater than 0 units and has distributed it at least once) Pools created with `token`. + """ + totalNumberOfActivePools: Int! + + """ + The number of memberships which have units allocated to them created with Pools that distribute `token`. + """ + totalMembershipsWithUnits: Int! + + """ + Counts all approved memberships whether or not they have units. + """ + totalConnectedMemberships: Int! + + """ + The total deposit held by all flow agreements for this particular `token`. """ totalDeposit: BigInt! """ - The total outflow rate of the `token` (how much value is being moved). + The total deposit held by the CFA for this particular `token`. + """ + totalCFADeposit: BigInt! + + """ + The total deposit held by the GDA agreement for this particular `token`. + """ + totalGDADeposit: BigInt! + + """ + The total outflow rate of the `token` (how much value is being moved) for all flow agreements. """ totalOutflowRate: BigInt! """ - The all-time total amount streamed (outflows) until the `updatedAtTimestamp`/`updatedAtBlock`. + The total outflow rate of the `token` (how much value is being moved) for the CFA. + """ + totalCFAOutflowRate: BigInt! + + """ + The total outflow rate of the `token` (how much value is being moved) for the GDA. + """ + totalGDAOutflowRate: BigInt! + + """ + The all-time total amount streamed (outflows) until the `updatedAtTimestamp`/`updatedAtBlock` for all flow agreements. """ totalAmountStreamedUntilUpdatedAt: BigInt! + """ + The all-time total amount streamed (outflows) until the `updatedAtTimestamp`/`updatedAtBlock` for the CFA. + """ + totalCFAAmountStreamedUntilUpdatedAt: BigInt! + + """ + The all-time total amount streamed (outflows) until the `updatedAtTimestamp`/`updatedAtBlock` for the GDA. + """ + totalGDAAmountStreamedUntilUpdatedAt: BigInt! + """ The all-time total amount transferred until the `updatedAtTimestamp`/`updatedAtBlock`. """ @@ -2233,15 +2907,35 @@ type TokenStatisticLog @entity { triggeredByEventName: String! # ---------------------------------- state ---------------------------------- """ - The total number of currently active `token` streams. + The total number of currently active `token` streams for all flow agreements. """ totalNumberOfActiveStreams: Int! """ - The count of closed streams for `token`. + The total number of currently active `token` streams for the CFA. + """ + totalCFANumberOfActiveStreams: Int! + + """ + The total number of currently active `token` streams for the GDA. + """ + totalGDANumberOfActiveStreams: Int! + + """ + The count of closed streams for `token` for all flow agreements. """ totalNumberOfClosedStreams: Int! + """ + The count of closed streams for `token` for the CFA. + """ + totalCFANumberOfClosedStreams: Int! + + """ + The count of closed streams for `token` for the GDA. + """ + totalGDANumberOfClosedStreams: Int! + """ The total number of Indexes created with `token`. """ @@ -2263,20 +2957,70 @@ type TokenStatisticLog @entity { totalApprovedSubscriptions: Int! """ - The total deposit held by the CFA agreement for this particular `token`. + The total number of Pools created with `token`. + """ + totalNumberOfPools: Int! + + """ + The total number of "active" (has greater than 0 units and has distributed it at least once) Pools created with `token`. + """ + totalNumberOfActivePools: Int! + + """ + The number of memberships which have units allocated to them created with Pools that distribute `token`. + """ + totalMembershipsWithUnits: Int! + + """ + Counts all connected memberships whether or not they have units. + """ + totalConnectedMemberships: Int! + + """ + The total deposit held by the CFA agreement for this particular `token` for all flow agreements. """ totalDeposit: BigInt! """ - The total outflow rate of the `token` (how much value is being moved). + The total deposit held by the CFA agreement for this particular `token` for the CFA. + """ + totalCFADeposit: BigInt! + + """ + The total deposit held by the CFA agreement for this particular `token` for the GDA. + """ + totalGDADeposit: BigInt! + + """ + The total outflow rate of the `token` (how much value is being moved) for all flow agreements. """ totalOutflowRate: BigInt! """ - The all-time total amount of `token` streamed (outflows) until the `timestamp`/`block`. + The total outflow rate of the `token` (how much value is being moved) for the CFA. + """ + totalCFAOutflowRate: BigInt! + + """ + The total outflow rate of the `token` (how much value is being moved) for the GDA. + """ + totalGDAOutflowRate: BigInt! + + """ + The all-time total amount of `token` streamed (outflows) until the `timestamp`/`block` for all flow agreements. """ totalAmountStreamed: BigInt! + """ + The all-time total amount of `token` streamed (outflows) until the `timestamp`/`block` for the CFA. + """ + totalCFAAmountStreamed: BigInt! + + """ + The all-time total amount of `token` streamed (outflows) until the `timestamp`/`block` for the GDA. + """ + totalGDAAmountStreamed: BigInt! + """ The all-time total amount of `token` transferred until the `timestamp`/`block`. """ diff --git a/packages/subgraph/scripts/buildNetworkConfig.ts b/packages/subgraph/scripts/buildNetworkConfig.ts index f3d938fc68..fbf0aec5f2 100644 --- a/packages/subgraph/scripts/buildNetworkConfig.ts +++ b/packages/subgraph/scripts/buildNetworkConfig.ts @@ -7,6 +7,7 @@ interface SubgraphConfig { readonly hostAddress: string; readonly cfaAddress: string; readonly idaAddress: string; + readonly gdaAddress: string; readonly superTokenFactoryAddress: string; readonly resolverV1Address: string; readonly nativeAssetSuperTokenAddress: string; @@ -33,6 +34,7 @@ function main() { hostAddress: networkMetadata.contractsV1.host, cfaAddress: networkMetadata.contractsV1.cfaV1, idaAddress: networkMetadata.contractsV1.idaV1, + gdaAddress: networkMetadata.contractsV1.gdaV1 || ADDRESS_ZERO, superTokenFactoryAddress: networkMetadata.contractsV1.superTokenFactory, resolverV1Address: networkMetadata.contractsV1.resolver, nativeAssetSuperTokenAddress: networkMetadata.nativeTokenWrapper, diff --git a/packages/subgraph/scripts/getAbi.js b/packages/subgraph/scripts/getAbi.js index 60a3c65834..b3502b1385 100755 --- a/packages/subgraph/scripts/getAbi.js +++ b/packages/subgraph/scripts/getAbi.js @@ -2,27 +2,22 @@ const fs = require("fs"); const path = require("path"); const contracts = [ - "ConstantFlowAgreementV1", "ERC20", "IConstantFlowAgreementV1", "IFlowNFTBase", - "IResolver", "ISuperTokenFactory", "ISuperToken", "ISuperfluid", "Resolver", "IInstantDistributionAgreementV1", - "InstantDistributionAgreementV1", + "IGeneralDistributionAgreementV1", + "ISuperfluidPool", "SuperfluidGovernanceBase", - "SuperToken", "TestToken", "TOGA", ]; -const directoryPath = path.join( - __dirname, - "../../ethereum-contracts/build/truffle" -); +const directoryPath = path.join(__dirname, "../../ethereum-contracts/build/truffle"); fs.mkdir("abis/", (err) => { if (err) return; //console.error(err); diff --git a/packages/subgraph/src/mappingHelpers.ts b/packages/subgraph/src/mappingHelpers.ts index 94380371e6..65632f86b6 100644 --- a/packages/subgraph/src/mappingHelpers.ts +++ b/packages/subgraph/src/mappingHelpers.ts @@ -7,6 +7,9 @@ import { FlowOperator, Index, IndexSubscription, + Pool, + PoolDistributor, + PoolMember, ResolverEntry, Stream, StreamRevision, @@ -30,6 +33,10 @@ import { getInitialTotalSupplyForSuperToken, ZERO_ADDRESS, handleTokenRPCCalls, + getPoolMemberID, + getPoolDistributorID, + getActiveStreamsDelta, + getClosedStreamsDelta, } from "./utils"; import { SuperToken as SuperTokenTemplate } from "../generated/templates"; import { ISuperToken as SuperToken } from "../generated/templates/SuperToken/ISuperToken"; @@ -478,6 +485,146 @@ export function getOrInitResolverEntry( return resolverEntry as ResolverEntry; } +export function getOrInitPool(event: ethereum.Event, poolId: string): Pool { + // get existing pool + let pool = Pool.load(poolId); + + // init new pool if non-existent + if (pool == null) { + pool = new Pool(poolId); + pool.createdAtTimestamp = event.block.timestamp; + pool.createdAtBlockNumber = event.block.number; + pool.updatedAtTimestamp = event.block.timestamp; + pool.updatedAtBlockNumber = event.block.number; + + pool.totalUnits = BIG_INT_ZERO; + pool.totalConnectedUnits = BIG_INT_ZERO; + pool.totalDisconnectedUnits = BIG_INT_ZERO; + pool.totalAmountInstantlyDistributedUntilUpdatedAt = BIG_INT_ZERO; + pool.totalAmountFlowedDistributedUntilUpdatedAt = BIG_INT_ZERO; + pool.totalAmountDistributedUntilUpdatedAt = BIG_INT_ZERO; + pool.totalMembers = 0; + pool.totalConnectedMembers = 0; + pool.totalDisconnectedMembers = 0; + pool.adjustmentFlowRate = BIG_INT_ZERO; + pool.flowRate = BIG_INT_ZERO; + pool.totalBuffer = BIG_INT_ZERO; + pool.token = ZERO_ADDRESS.toHex(); + pool.admin = ZERO_ADDRESS.toHex(); + } + + return pool; +} + +export function updatePoolTotalAmountFlowedAndDistributed( + event: ethereum.Event, + pool: Pool +): Pool { + const timeDelta = event.block.timestamp.minus(pool.updatedAtTimestamp); + const amountFlowedSinceLastUpdate = pool.flowRate.times(timeDelta); + + pool.updatedAtBlockNumber = event.block.number; + pool.updatedAtTimestamp = event.block.timestamp; + + pool.totalAmountFlowedDistributedUntilUpdatedAt = + pool.totalAmountFlowedDistributedUntilUpdatedAt.plus( + amountFlowedSinceLastUpdate + ); + pool.totalAmountDistributedUntilUpdatedAt = + pool.totalAmountDistributedUntilUpdatedAt.plus( + amountFlowedSinceLastUpdate + ); + + pool.save(); + + return pool; +} + +export function getOrInitPoolMember( + event: ethereum.Event, + poolAddress: Address, + poolMemberAddress: Address +): PoolMember { + const poolMemberID = getPoolMemberID(poolAddress, poolMemberAddress); + let poolMember = PoolMember.load(poolMemberID); + + if (poolMember == null) { + poolMember = new PoolMember(poolMemberID); + poolMember.createdAtTimestamp = event.block.timestamp; + poolMember.createdAtBlockNumber = event.block.number; + poolMember.updatedAtTimestamp = event.block.timestamp; + poolMember.updatedAtBlockNumber = event.block.number; + + poolMember.units = BIG_INT_ZERO; + poolMember.isConnected = false; + poolMember.totalAmountClaimed = BIG_INT_ZERO; + + poolMember.account = poolMemberAddress.toHex(); + poolMember.pool = poolAddress.toHex(); + } + + return poolMember; +} + +export function getOrInitPoolDistributor( + event: ethereum.Event, + poolAddress: Address, + poolDistributorAddress: Address +): PoolDistributor { + const poolDistributorID = getPoolDistributorID( + poolAddress, + poolDistributorAddress + ); + let poolDistributor = PoolDistributor.load(poolDistributorID); + + if (poolDistributor == null) { + poolDistributor = new PoolDistributor(poolDistributorID); + poolDistributor.createdAtTimestamp = event.block.timestamp; + poolDistributor.createdAtBlockNumber = event.block.number; + poolDistributor.updatedAtTimestamp = event.block.timestamp; + poolDistributor.updatedAtBlockNumber = event.block.number; + + poolDistributor.totalAmountInstantlyDistributedUntilUpdatedAt = + BIG_INT_ZERO; + poolDistributor.totalAmountFlowedDistributedUntilUpdatedAt = + BIG_INT_ZERO; + poolDistributor.totalAmountDistributedUntilUpdatedAt = BIG_INT_ZERO; + poolDistributor.totalBuffer = BIG_INT_ZERO; + poolDistributor.flowRate = BIG_INT_ZERO; + + poolDistributor.account = poolDistributorAddress.toHex(); + poolDistributor.pool = poolAddress.toHex(); + } + + return poolDistributor; +} +export function updatePoolDistributorTotalAmountFlowedAndDistributed( + event: ethereum.Event, + poolDistributor: PoolDistributor +): PoolDistributor { + const timeDelta = event.block.timestamp.minus( + poolDistributor.updatedAtTimestamp + ); + const amountFlowedSinceLastUpdate = + poolDistributor.flowRate.times(timeDelta); + + poolDistributor.updatedAtBlockNumber = event.block.number; + poolDistributor.updatedAtTimestamp = event.block.timestamp; + + poolDistributor.totalAmountFlowedDistributedUntilUpdatedAt = + poolDistributor.totalAmountFlowedDistributedUntilUpdatedAt.plus( + amountFlowedSinceLastUpdate + ); + poolDistributor.totalAmountDistributedUntilUpdatedAt = + poolDistributor.totalAmountDistributedUntilUpdatedAt.plus( + amountFlowedSinceLastUpdate + ); + + poolDistributor.save(); + + return poolDistributor; +} + /************************************************************************** * Aggregate initializer functions *************************************************************************/ @@ -494,25 +641,59 @@ if (accountTokenSnapshot == null) { accountTokenSnapshot.updatedAtTimestamp = block.timestamp; accountTokenSnapshot.updatedAtBlockNumber = block.number; accountTokenSnapshot.totalNumberOfActiveStreams = 0; + accountTokenSnapshot.totalCFANumberOfActiveStreams = 0; + accountTokenSnapshot.totalGDANumberOfActiveStreams = 0; accountTokenSnapshot.activeIncomingStreamCount = 0; + accountTokenSnapshot.activeCFAIncomingStreamCount = 0; + accountTokenSnapshot.activeGDAIncomingStreamCount = 0; accountTokenSnapshot.activeOutgoingStreamCount = 0; + accountTokenSnapshot.activeCFAOutgoingStreamCount = 0; + accountTokenSnapshot.activeGDAOutgoingStreamCount = 0; accountTokenSnapshot.inactiveIncomingStreamCount = 0; + accountTokenSnapshot.inactiveCFAIncomingStreamCount = 0; + accountTokenSnapshot.inactiveGDAIncomingStreamCount = 0; accountTokenSnapshot.inactiveOutgoingStreamCount = 0; + accountTokenSnapshot.inactiveCFAOutgoingStreamCount = 0; + accountTokenSnapshot.inactiveGDAOutgoingStreamCount = 0; accountTokenSnapshot.totalNumberOfClosedStreams = 0; - accountTokenSnapshot.totalSubscriptionsWithUnits = 0; + accountTokenSnapshot.totalCFANumberOfClosedStreams = 0; + accountTokenSnapshot.totalGDANumberOfClosedStreams = 0; accountTokenSnapshot.isLiquidationEstimateOptimistic = false; + accountTokenSnapshot.totalSubscriptionsWithUnits = 0; accountTokenSnapshot.totalApprovedSubscriptions = 0; + accountTokenSnapshot.totalMembershipsWithUnits = 0; + accountTokenSnapshot.totalConnectedMemberships = 0; accountTokenSnapshot.balanceUntilUpdatedAt = BIG_INT_ZERO; accountTokenSnapshot.totalNetFlowRate = BIG_INT_ZERO; + accountTokenSnapshot.totalCFANetFlowRate = BIG_INT_ZERO; + accountTokenSnapshot.totalGDANetFlowRate = BIG_INT_ZERO; accountTokenSnapshot.totalInflowRate = BIG_INT_ZERO; + accountTokenSnapshot.totalCFAInflowRate = BIG_INT_ZERO; + accountTokenSnapshot.totalGDAInflowRate = BIG_INT_ZERO; accountTokenSnapshot.totalOutflowRate = BIG_INT_ZERO; + accountTokenSnapshot.totalCFAOutflowRate = BIG_INT_ZERO; + accountTokenSnapshot.totalGDAOutflowRate = BIG_INT_ZERO; accountTokenSnapshot.totalAmountStreamedInUntilUpdatedAt = BIG_INT_ZERO; + accountTokenSnapshot.totalCFAAmountStreamedInUntilUpdatedAt = + BIG_INT_ZERO; + accountTokenSnapshot.totalGDAAmountStreamedInUntilUpdatedAt = + BIG_INT_ZERO; accountTokenSnapshot.totalAmountStreamedOutUntilUpdatedAt = BIG_INT_ZERO; + accountTokenSnapshot.totalCFAAmountStreamedOutUntilUpdatedAt = + BIG_INT_ZERO; + accountTokenSnapshot.totalGDAAmountStreamedOutUntilUpdatedAt = + BIG_INT_ZERO; accountTokenSnapshot.totalAmountStreamedUntilUpdatedAt = BIG_INT_ZERO; + accountTokenSnapshot.totalCFAAmountStreamedUntilUpdatedAt = + BIG_INT_ZERO; + accountTokenSnapshot.totalGDAAmountStreamedUntilUpdatedAt = + BIG_INT_ZERO; accountTokenSnapshot.totalAmountTransferredUntilUpdatedAt = BIG_INT_ZERO; accountTokenSnapshot.totalDeposit = BIG_INT_ZERO; + accountTokenSnapshot.totalCFADeposit = BIG_INT_ZERO; + accountTokenSnapshot.totalGDADeposit = BIG_INT_ZERO; accountTokenSnapshot.maybeCriticalAtTimestamp = null; accountTokenSnapshot.account = accountAddress.toHex(); accountTokenSnapshot.token = tokenAddress.toHex(); @@ -523,7 +704,7 @@ if (accountTokenSnapshot == null) { tokenStatistic.save(); } - + return accountTokenSnapshot as AccountTokenSnapshot; } @@ -536,14 +717,14 @@ export function _createAccountTokenSnapshotLogEntity( if (accountAddress.equals(ZERO_ADDRESS)) { return; } - const accountTokenSnapshot = getOrInitAccountTokenSnapshot( + const ats = getOrInitAccountTokenSnapshot( accountAddress, tokenAddress, event.block ); // Transaction const atsLog = new AccountTokenSnapshotLog( - createLogID("ATSLog", accountTokenSnapshot.id, event) + createLogID("ATSLog", ats.id, event) ); atsLog.transactionHash = event.transaction.hash; atsLog.timestamp = event.block.timestamp; @@ -552,40 +733,59 @@ export function _createAccountTokenSnapshotLogEntity( atsLog.logIndex = event.logIndex; atsLog.triggeredByEventName = eventName; // Account token snapshot state - atsLog.totalNumberOfActiveStreams = - accountTokenSnapshot.totalNumberOfActiveStreams; - atsLog.activeIncomingStreamCount = - accountTokenSnapshot.activeIncomingStreamCount; - atsLog.activeOutgoingStreamCount = - accountTokenSnapshot.activeOutgoingStreamCount; - atsLog.totalNumberOfClosedStreams = - accountTokenSnapshot.totalNumberOfClosedStreams; - atsLog.inactiveIncomingStreamCount = - accountTokenSnapshot.inactiveIncomingStreamCount; - atsLog.inactiveOutgoingStreamCount = - accountTokenSnapshot.inactiveOutgoingStreamCount; - atsLog.totalSubscriptionsWithUnits = - accountTokenSnapshot.totalSubscriptionsWithUnits; - atsLog.totalApprovedSubscriptions = - accountTokenSnapshot.totalApprovedSubscriptions; - atsLog.balance = accountTokenSnapshot.balanceUntilUpdatedAt; - atsLog.totalNetFlowRate = accountTokenSnapshot.totalNetFlowRate; - atsLog.totalInflowRate = accountTokenSnapshot.totalInflowRate; - atsLog.totalOutflowRate = accountTokenSnapshot.totalOutflowRate; - atsLog.totalAmountStreamed = - accountTokenSnapshot.totalAmountStreamedUntilUpdatedAt; - atsLog.totalAmountStreamedIn = - accountTokenSnapshot.totalAmountStreamedInUntilUpdatedAt; - atsLog.totalAmountStreamedOut = - accountTokenSnapshot.totalAmountStreamedOutUntilUpdatedAt; - atsLog.totalAmountTransferred = - accountTokenSnapshot.totalAmountTransferredUntilUpdatedAt; - atsLog.totalDeposit = accountTokenSnapshot.totalDeposit; - atsLog.maybeCriticalAtTimestamp = - accountTokenSnapshot.maybeCriticalAtTimestamp; - atsLog.account = accountTokenSnapshot.account; - atsLog.token = accountTokenSnapshot.token; - atsLog.accountTokenSnapshot = accountTokenSnapshot.id; + atsLog.totalNumberOfActiveStreams = ats.totalNumberOfActiveStreams; + atsLog.totalCFANumberOfActiveStreams = ats.totalCFANumberOfActiveStreams; + atsLog.totalGDANumberOfActiveStreams = ats.totalGDANumberOfActiveStreams; + atsLog.activeIncomingStreamCount = ats.activeIncomingStreamCount; + atsLog.activeCFAIncomingStreamCount = ats.activeCFAIncomingStreamCount; + atsLog.activeGDAIncomingStreamCount = ats.activeGDAIncomingStreamCount; + atsLog.activeOutgoingStreamCount = ats.activeOutgoingStreamCount; + atsLog.activeCFAOutgoingStreamCount = ats.activeCFAOutgoingStreamCount; + atsLog.activeGDAOutgoingStreamCount = ats.activeGDAOutgoingStreamCount; + atsLog.totalNumberOfClosedStreams = ats.totalNumberOfClosedStreams; + atsLog.totalCFANumberOfClosedStreams = ats.totalCFANumberOfClosedStreams; + atsLog.totalGDANumberOfClosedStreams = ats.totalGDANumberOfClosedStreams; + atsLog.inactiveIncomingStreamCount = ats.inactiveIncomingStreamCount; + atsLog.inactiveCFAIncomingStreamCount = ats.inactiveCFAIncomingStreamCount; + atsLog.inactiveGDAIncomingStreamCount = ats.inactiveGDAIncomingStreamCount; + atsLog.inactiveOutgoingStreamCount = ats.inactiveOutgoingStreamCount; + atsLog.inactiveCFAOutgoingStreamCount = ats.inactiveCFAOutgoingStreamCount; + atsLog.inactiveGDAOutgoingStreamCount = ats.inactiveGDAOutgoingStreamCount; + atsLog.totalSubscriptionsWithUnits = ats.totalSubscriptionsWithUnits; + atsLog.totalApprovedSubscriptions = ats.totalApprovedSubscriptions; + atsLog.totalMembershipsWithUnits = ats.totalMembershipsWithUnits; + atsLog.totalConnectedMemberships = ats.totalConnectedMemberships; + atsLog.balance = ats.balanceUntilUpdatedAt; + atsLog.totalNetFlowRate = ats.totalNetFlowRate; + atsLog.totalCFANetFlowRate = ats.totalCFANetFlowRate; + atsLog.totalGDANetFlowRate = ats.totalGDANetFlowRate; + atsLog.totalInflowRate = ats.totalInflowRate; + atsLog.totalCFAInflowRate = ats.totalCFAInflowRate; + atsLog.totalGDAInflowRate = ats.totalGDAInflowRate; + atsLog.totalOutflowRate = ats.totalOutflowRate; + atsLog.totalCFAOutflowRate = ats.totalCFAOutflowRate; + atsLog.totalGDAOutflowRate = ats.totalGDAOutflowRate; + atsLog.totalAmountStreamed = ats.totalAmountStreamedUntilUpdatedAt; + atsLog.totalCFAAmountStreamed = ats.totalCFAAmountStreamedUntilUpdatedAt; + atsLog.totalGDAAmountStreamed = ats.totalGDAAmountStreamedUntilUpdatedAt; + atsLog.totalAmountStreamedIn = ats.totalAmountStreamedInUntilUpdatedAt; + atsLog.totalCFAAmountStreamedIn = + ats.totalCFAAmountStreamedInUntilUpdatedAt; + atsLog.totalAmountStreamedOut = ats.totalAmountStreamedOutUntilUpdatedAt; + atsLog.totalGDAAmountStreamedIn = + ats.totalGDAAmountStreamedInUntilUpdatedAt; + atsLog.totalCFAAmountStreamedOut = + ats.totalCFAAmountStreamedOutUntilUpdatedAt; + atsLog.totalGDAAmountStreamedOut = + ats.totalGDAAmountStreamedOutUntilUpdatedAt; + atsLog.totalAmountTransferred = ats.totalAmountTransferredUntilUpdatedAt; + atsLog.totalDeposit = ats.totalDeposit; + atsLog.totalCFADeposit = ats.totalCFADeposit; + atsLog.totalGDADeposit = ats.totalGDADeposit; + atsLog.maybeCriticalAtTimestamp = ats.maybeCriticalAtTimestamp; + atsLog.account = ats.account; + atsLog.token = ats.token; + atsLog.accountTokenSnapshot = ats.id; atsLog.save(); } @@ -600,18 +800,32 @@ export function getOrInitTokenStatistic( tokenStatistic.updatedAtTimestamp = block.timestamp; tokenStatistic.updatedAtBlockNumber = block.number; tokenStatistic.totalNumberOfActiveStreams = 0; + tokenStatistic.totalCFANumberOfActiveStreams = 0; + tokenStatistic.totalGDANumberOfActiveStreams = 0; tokenStatistic.totalNumberOfClosedStreams = 0; + tokenStatistic.totalCFANumberOfClosedStreams = 0; + tokenStatistic.totalGDANumberOfClosedStreams = 0; tokenStatistic.totalNumberOfIndexes = 0; tokenStatistic.totalNumberOfActiveIndexes = 0; tokenStatistic.totalSubscriptionsWithUnits = 0; tokenStatistic.totalApprovedSubscriptions = 0; + tokenStatistic.totalNumberOfPools = 0; + tokenStatistic.totalNumberOfActivePools = 0; + tokenStatistic.totalMembershipsWithUnits = 0; + tokenStatistic.totalConnectedMemberships = 0; tokenStatistic.totalOutflowRate = BIG_INT_ZERO; + tokenStatistic.totalCFAOutflowRate = BIG_INT_ZERO; + tokenStatistic.totalGDAOutflowRate = BIG_INT_ZERO; tokenStatistic.totalNumberOfAccounts = 0; tokenStatistic.totalAmountStreamedUntilUpdatedAt = BIG_INT_ZERO; + tokenStatistic.totalCFAAmountStreamedUntilUpdatedAt = BIG_INT_ZERO; + tokenStatistic.totalGDAAmountStreamedUntilUpdatedAt = BIG_INT_ZERO; tokenStatistic.totalAmountTransferredUntilUpdatedAt = BIG_INT_ZERO; tokenStatistic.totalAmountDistributedUntilUpdatedAt = BIG_INT_ZERO; tokenStatistic.totalSupply = BIG_INT_ZERO; tokenStatistic.totalDeposit = BIG_INT_ZERO; + tokenStatistic.totalCFADeposit = BIG_INT_ZERO; + tokenStatistic.totalGDADeposit = BIG_INT_ZERO; tokenStatistic.totalNumberOfHolders = 0; tokenStatistic.token = tokenId; tokenStatistic.save(); @@ -640,8 +854,16 @@ export function _createTokenStatisticLogEntity( // Token Statistic State tokenStatisticLog.totalNumberOfActiveStreams = tokenStatistic.totalNumberOfActiveStreams; + tokenStatisticLog.totalCFANumberOfActiveStreams = + tokenStatistic.totalCFANumberOfActiveStreams; + tokenStatisticLog.totalGDANumberOfActiveStreams = + tokenStatistic.totalGDANumberOfActiveStreams; tokenStatisticLog.totalNumberOfClosedStreams = tokenStatistic.totalNumberOfClosedStreams; + tokenStatisticLog.totalCFANumberOfClosedStreams = + tokenStatistic.totalCFANumberOfClosedStreams; + tokenStatisticLog.totalGDANumberOfClosedStreams = + tokenStatistic.totalGDANumberOfClosedStreams; tokenStatisticLog.totalNumberOfIndexes = tokenStatistic.totalNumberOfIndexes; tokenStatisticLog.totalNumberOfActiveIndexes = @@ -650,10 +872,25 @@ export function _createTokenStatisticLogEntity( tokenStatistic.totalSubscriptionsWithUnits; tokenStatisticLog.totalApprovedSubscriptions = tokenStatistic.totalApprovedSubscriptions; + tokenStatisticLog.totalNumberOfPools = tokenStatistic.totalNumberOfPools; + tokenStatisticLog.totalNumberOfActivePools = + tokenStatistic.totalNumberOfActivePools; + tokenStatisticLog.totalMembershipsWithUnits = + tokenStatistic.totalMembershipsWithUnits; + tokenStatisticLog.totalConnectedMemberships = + tokenStatistic.totalConnectedMemberships; tokenStatisticLog.totalDeposit = tokenStatistic.totalDeposit; + tokenStatisticLog.totalCFADeposit = tokenStatistic.totalCFADeposit; + tokenStatisticLog.totalGDADeposit = tokenStatistic.totalGDADeposit; tokenStatisticLog.totalOutflowRate = tokenStatistic.totalOutflowRate; + tokenStatisticLog.totalCFAOutflowRate = tokenStatistic.totalCFAOutflowRate; + tokenStatisticLog.totalGDAOutflowRate = tokenStatistic.totalGDAOutflowRate; tokenStatisticLog.totalAmountStreamed = tokenStatistic.totalAmountStreamedUntilUpdatedAt; + tokenStatisticLog.totalCFAAmountStreamed = + tokenStatistic.totalCFAAmountStreamedUntilUpdatedAt; + tokenStatisticLog.totalGDAAmountStreamed = + tokenStatistic.totalGDAAmountStreamedUntilUpdatedAt; tokenStatisticLog.totalAmountTransferred = tokenStatistic.totalAmountTransferredUntilUpdatedAt; tokenStatisticLog.totalAmountDistributed = @@ -688,9 +925,9 @@ export function updateAccountUpdatedAt( *************************************************************************/ /** - * Updates ATS and TokenStats IDA Subscriptions data. + * Updates ATS and TokenStats distribution agreement data (IDA or GDA). */ -export function updateAggregateIDASubscriptionsData( +export function updateAggregateDistributionAgreementData( accountAddress: Address, tokenAddress: Address, subscriptionWithUnitsExists: boolean, @@ -699,15 +936,18 @@ export function updateAggregateIDASubscriptionsData( isRevokingSubscription: boolean, isDeletingSubscription: boolean, isApproving: boolean, - block: ethereum.Block + block: ethereum.Block, + isIDA: boolean ): void { - const tokenStatistic = getOrInitTokenStatistic(tokenAddress, block); const totalSubscriptionWithUnitsDelta = + // we only decrement if the subscription exists and we are deleting isDeletingSubscription && subscriptionWithUnitsExists ? -1 - : isIncrementingSubWithUnits && !subscriptionWithUnitsExists + : // we only increment if the subscription does not exist and we are incrementing + isIncrementingSubWithUnits && !subscriptionWithUnitsExists ? 1 : 0; + const totalApprovedSubscriptionsDelta = isApproving ? 1 : isRevokingSubscription && subscriptionApproved @@ -721,28 +961,47 @@ export function updateAggregateIDASubscriptionsData( block ); - accountTokenSnapshot.totalSubscriptionsWithUnits = - accountTokenSnapshot.totalSubscriptionsWithUnits + - totalSubscriptionWithUnitsDelta; + if (isIDA) { + accountTokenSnapshot.totalSubscriptionsWithUnits = + accountTokenSnapshot.totalSubscriptionsWithUnits + + totalSubscriptionWithUnitsDelta; + accountTokenSnapshot.totalApprovedSubscriptions = + accountTokenSnapshot.totalApprovedSubscriptions + + totalApprovedSubscriptionsDelta; + } else { + accountTokenSnapshot.totalMembershipsWithUnits = + accountTokenSnapshot.totalMembershipsWithUnits + + totalSubscriptionWithUnitsDelta; + accountTokenSnapshot.totalConnectedMemberships = + accountTokenSnapshot.totalConnectedMemberships + + totalApprovedSubscriptionsDelta; + } + accountTokenSnapshot.isLiquidationEstimateOptimistic = - accountTokenSnapshot.totalSubscriptionsWithUnits > 0; - accountTokenSnapshot.totalApprovedSubscriptions = - accountTokenSnapshot.totalApprovedSubscriptions + - totalApprovedSubscriptionsDelta; + accountTokenSnapshot.totalSubscriptionsWithUnits > 0 || + accountTokenSnapshot.totalMembershipsWithUnits > 0; accountTokenSnapshot.updatedAtTimestamp = block.timestamp; accountTokenSnapshot.updatedAtBlockNumber = block.number; - accountTokenSnapshot.save(); - // update tokenStatistic Subscription data - tokenStatistic.totalSubscriptionsWithUnits = - tokenStatistic.totalSubscriptionsWithUnits + - totalSubscriptionWithUnitsDelta; - accountTokenSnapshot.isLiquidationEstimateOptimistic = - accountTokenSnapshot.totalSubscriptionsWithUnits > 0; - tokenStatistic.totalApprovedSubscriptions = - tokenStatistic.totalApprovedSubscriptions + - totalApprovedSubscriptionsDelta; + // update TokenStatistic entity + const tokenStatistic = getOrInitTokenStatistic(tokenAddress, block); + if (isIDA) { + tokenStatistic.totalSubscriptionsWithUnits = + tokenStatistic.totalSubscriptionsWithUnits + + totalSubscriptionWithUnitsDelta; + tokenStatistic.totalApprovedSubscriptions = + tokenStatistic.totalApprovedSubscriptions + + totalApprovedSubscriptionsDelta; + } else { + tokenStatistic.totalMembershipsWithUnits = + tokenStatistic.totalMembershipsWithUnits + + totalSubscriptionWithUnitsDelta; + tokenStatistic.totalConnectedMemberships = + tokenStatistic.totalConnectedMemberships + + totalApprovedSubscriptionsDelta; + } + tokenStatistic.updatedAtTimestamp = block.timestamp; tokenStatistic.updatedAtBlockNumber = block.number; @@ -821,20 +1080,21 @@ export function updateATSStreamedAndBalanceUntilUpdatedAt( ); const balanceUntilUpdatedAtBeforeUpdate = accountTokenSnapshot.balanceUntilUpdatedAt; - - const amountStreamedSinceLastUpdatedAt = + + //////////////// CFA + GDA streamed amounts //////////////// + const totalAmountStreamedSinceLastUpdatedAt = getAmountStreamedSinceLastUpdatedAt( block.timestamp, accountTokenSnapshot.updatedAtTimestamp, accountTokenSnapshot.totalNetFlowRate ); - const amountStreamedInSinceLastUpdatedAt = + const totalAmountStreamedInSinceLastUpdatedAt = getAmountStreamedSinceLastUpdatedAt( block.timestamp, accountTokenSnapshot.updatedAtTimestamp, accountTokenSnapshot.totalInflowRate ); - const amountStreamedOutSinceLastUpdatedAt = + const totalAmountStreamedOutSinceLastUpdatedAt = getAmountStreamedSinceLastUpdatedAt( block.timestamp, accountTokenSnapshot.updatedAtTimestamp, @@ -844,26 +1104,19 @@ export function updateATSStreamedAndBalanceUntilUpdatedAt( // update the totalStreamedUntilUpdatedAt (net) accountTokenSnapshot.totalAmountStreamedUntilUpdatedAt = accountTokenSnapshot.totalAmountStreamedUntilUpdatedAt.plus( - amountStreamedSinceLastUpdatedAt + totalAmountStreamedSinceLastUpdatedAt ); // update the totalStreamedUntilUpdatedAt (in) accountTokenSnapshot.totalAmountStreamedInUntilUpdatedAt = accountTokenSnapshot.totalAmountStreamedInUntilUpdatedAt.plus( - amountStreamedInSinceLastUpdatedAt + totalAmountStreamedInSinceLastUpdatedAt ); // update the totalStreamedUntilUpdatedAt (out) accountTokenSnapshot.totalAmountStreamedOutUntilUpdatedAt = accountTokenSnapshot.totalAmountStreamedOutUntilUpdatedAt.plus( - amountStreamedOutSinceLastUpdatedAt - ); - - const netAmountStreamedInSinceLastUpdatedAt = - getAmountStreamedSinceLastUpdatedAt( - block.timestamp, - accountTokenSnapshot.updatedAtTimestamp, - accountTokenSnapshot.totalNetFlowRate + totalAmountStreamedOutSinceLastUpdatedAt ); // update the balance via external call if account has any subscription with more than 0 units @@ -873,9 +1126,86 @@ export function updateATSStreamedAndBalanceUntilUpdatedAt( accountTokenSnapshot, block, balanceDelta - ? balanceDelta.plus(netAmountStreamedInSinceLastUpdatedAt) + ? balanceDelta.plus(totalAmountStreamedSinceLastUpdatedAt) : balanceDelta ); + + //////////////// CFA streamed amounts //////////////// + const totalCFAAmountStreamedSinceLastUpdatedAt = + getAmountStreamedSinceLastUpdatedAt( + block.timestamp, + accountTokenSnapshot.updatedAtTimestamp, + accountTokenSnapshot.totalCFANetFlowRate + ); + const totalCFAAmountStreamedInSinceLastUpdatedAt = + getAmountStreamedSinceLastUpdatedAt( + block.timestamp, + accountTokenSnapshot.updatedAtTimestamp, + accountTokenSnapshot.totalCFAInflowRate + ); + const totalCFAAmountStreamedOutSinceLastUpdatedAt = + getAmountStreamedSinceLastUpdatedAt( + block.timestamp, + accountTokenSnapshot.updatedAtTimestamp, + accountTokenSnapshot.totalCFAOutflowRate + ); + + // update the totalCFAStreamedUntilUpdatedAt (net) + accountTokenSnapshot.totalCFAAmountStreamedUntilUpdatedAt = + accountTokenSnapshot.totalCFAAmountStreamedUntilUpdatedAt.plus( + totalCFAAmountStreamedSinceLastUpdatedAt + ); + + // update the totalCFAStreamedUntilUpdatedAt (in) + accountTokenSnapshot.totalCFAAmountStreamedInUntilUpdatedAt = + accountTokenSnapshot.totalCFAAmountStreamedInUntilUpdatedAt.plus( + totalCFAAmountStreamedInSinceLastUpdatedAt + ); + + // update the totalCFAStreamedUntilUpdatedAt (out) + accountTokenSnapshot.totalCFAAmountStreamedOutUntilUpdatedAt = + accountTokenSnapshot.totalCFAAmountStreamedOutUntilUpdatedAt.plus( + totalCFAAmountStreamedOutSinceLastUpdatedAt + ); + + //////////////// GDA streamed amounts //////////////// + const totalGDAAmountStreamedSinceLastUpdatedAt = + getAmountStreamedSinceLastUpdatedAt( + block.timestamp, + accountTokenSnapshot.updatedAtTimestamp, + accountTokenSnapshot.totalGDANetFlowRate + ); + const totalGDAAmountStreamedInSinceLastUpdatedAt = + getAmountStreamedSinceLastUpdatedAt( + block.timestamp, + accountTokenSnapshot.updatedAtTimestamp, + accountTokenSnapshot.totalGDAInflowRate + ); + const totalGDAAmountStreamedOutSinceLastUpdatedAt = + getAmountStreamedSinceLastUpdatedAt( + block.timestamp, + accountTokenSnapshot.updatedAtTimestamp, + accountTokenSnapshot.totalGDAOutflowRate + ); + + // update the totalGDAStreamedUntilUpdatedAt (net) + accountTokenSnapshot.totalGDAAmountStreamedUntilUpdatedAt = + accountTokenSnapshot.totalGDAAmountStreamedUntilUpdatedAt.plus( + totalGDAAmountStreamedSinceLastUpdatedAt + ); + + // update the totalGDAStreamedUntilUpdatedAt (in) + accountTokenSnapshot.totalGDAAmountStreamedInUntilUpdatedAt = + accountTokenSnapshot.totalGDAAmountStreamedInUntilUpdatedAt.plus( + totalGDAAmountStreamedInSinceLastUpdatedAt + ); + + // update the totalGDAStreamedUntilUpdatedAt (out) + accountTokenSnapshot.totalGDAAmountStreamedOutUntilUpdatedAt = + accountTokenSnapshot.totalGDAAmountStreamedOutUntilUpdatedAt.plus( + totalGDAAmountStreamedOutSinceLastUpdatedAt + ); + accountTokenSnapshot.save(); const balanceUntilUpdatedAtAfterUpdate = accountTokenSnapshot.balanceUntilUpdatedAt; @@ -908,7 +1238,9 @@ export function updateATSStreamedAndBalanceUntilUpdatedAt( } /** - * This function should always be called with updateATSStreamedAndBalanceUntilUpdatedAt + * This function updates the token stats streamed amounts as well as the + * updatedAtTimestamp and updatedAtBlockNumber. + * It should always be called with updateATSStreamedAndBalanceUntilUpdatedAt. * @param tokenAddress * @param block */ @@ -917,6 +1249,8 @@ export function updateTokenStatsStreamedUntilUpdatedAt( block: ethereum.Block ): void { const tokenStats = getOrInitTokenStatistic(tokenAddress, block); + + //// CFA + GDA streamed amounts //// const amountStreamedSinceLastUpdatedAt = getAmountStreamedSinceLastUpdatedAt( block.timestamp, @@ -927,36 +1261,52 @@ export function updateTokenStatsStreamedUntilUpdatedAt( tokenStats.totalAmountStreamedUntilUpdatedAt.plus( amountStreamedSinceLastUpdatedAt ); + + //// CFA streamed amounts //// + const cfaAmountStreamedSinceLastUpdatedAt = + getAmountStreamedSinceLastUpdatedAt( + block.timestamp, + tokenStats.updatedAtTimestamp, + tokenStats.totalCFAOutflowRate + ); + tokenStats.totalCFAAmountStreamedUntilUpdatedAt = + tokenStats.totalCFAAmountStreamedUntilUpdatedAt.plus( + cfaAmountStreamedSinceLastUpdatedAt + ); + + //// GDA streamed amounts //// + const gdaAmountStreamedSinceLastUpdatedAt = + getAmountStreamedSinceLastUpdatedAt( + block.timestamp, + tokenStats.updatedAtTimestamp, + tokenStats.totalGDAOutflowRate + ); + tokenStats.totalGDAAmountStreamedUntilUpdatedAt = + tokenStats.totalGDAAmountStreamedUntilUpdatedAt.plus( + gdaAmountStreamedSinceLastUpdatedAt + ); + tokenStats.updatedAtTimestamp = block.timestamp; tokenStats.updatedAtBlockNumber = block.number; tokenStats.save(); } -/** - * Updates TokenStatistic and AccountTokenSnapshot countable stream - * data. Must be called after updating streamed amount data for the - * AccountTokenSnapshot entities. - */ -export function updateAggregateEntitiesStreamData( - senderAddress: Address, - receiverAddress: Address, +export function updateTokenStatisticStreamData( tokenAddress: Address, newFlowRate: BigInt, flowRateDelta: BigInt, depositDelta: BigInt, isCreate: boolean, isDelete: boolean, + isCFA: boolean, block: ethereum.Block ): void { const tokenStatistic = getOrInitTokenStatistic(tokenAddress, block); - const totalNumberOfActiveStreamsDelta = isCreate ? 1 : isDelete ? -1 : 0; - const totalNumberOfClosedStreamsDelta = isDelete ? 1 : 0; - const tokenStatsAmountStreamedSinceLastUpdate = - getAmountStreamedSinceLastUpdatedAt( - block.timestamp, - tokenStatistic.updatedAtTimestamp, - tokenStatistic.totalOutflowRate - ); + const totalNumberOfActiveStreamsDelta = getActiveStreamsDelta( + isCreate, + isDelete + ); + const totalNumberOfClosedStreamsDelta = getClosedStreamsDelta(isDelete); // the outflow rate should never go below 0. tokenStatistic.totalOutflowRate = tokenStatistic.totalOutflowRate @@ -973,16 +1323,68 @@ export function updateAggregateEntitiesStreamData( tokenStatistic.totalNumberOfClosedStreams + totalNumberOfClosedStreamsDelta; - tokenStatistic.totalAmountStreamedUntilUpdatedAt = - tokenStatistic.totalAmountStreamedUntilUpdatedAt.plus( - tokenStatsAmountStreamedSinceLastUpdate - ); - tokenStatistic.updatedAtTimestamp = block.timestamp; - tokenStatistic.updatedAtBlockNumber = block.number; - tokenStatistic.totalDeposit = tokenStatistic.totalDeposit.plus(depositDelta); + if (isCFA) { + tokenStatistic.totalCFAOutflowRate = tokenStatistic.totalCFAOutflowRate + .plus(flowRateDelta) + .lt(BIG_INT_ZERO) + ? newFlowRate + : tokenStatistic.totalCFAOutflowRate.plus(flowRateDelta); + + tokenStatistic.totalCFANumberOfActiveStreams = + tokenStatistic.totalCFANumberOfActiveStreams + + totalNumberOfActiveStreamsDelta; + + tokenStatistic.totalCFANumberOfClosedStreams = + tokenStatistic.totalCFANumberOfClosedStreams + + totalNumberOfClosedStreamsDelta; + + tokenStatistic.totalCFADeposit = + tokenStatistic.totalCFADeposit.plus(depositDelta); + } else { + tokenStatistic.totalGDAOutflowRate = tokenStatistic.totalGDAOutflowRate + .plus(flowRateDelta) + .lt(BIG_INT_ZERO) + ? newFlowRate + : tokenStatistic.totalGDAOutflowRate.plus(flowRateDelta); + + tokenStatistic.totalGDANumberOfActiveStreams = + tokenStatistic.totalGDANumberOfActiveStreams + + totalNumberOfActiveStreamsDelta; + + tokenStatistic.totalGDANumberOfClosedStreams = + tokenStatistic.totalGDANumberOfClosedStreams + + totalNumberOfClosedStreamsDelta; + + tokenStatistic.totalGDADeposit = + tokenStatistic.totalGDADeposit.plus(depositDelta); + } + tokenStatistic.save(); +} + +/** + * Updates ATS stream counter data. + * Must be called after updating streamed amount data for the + * AccountTokenSnapshot entities. + */ +export function updateSenderATSStreamData( + senderAddress: Address, + tokenAddress: Address, + newFlowRate: BigInt, + flowRateDelta: BigInt, + depositDelta: BigInt, + isCreate: boolean, + isDelete: boolean, + isCFA: boolean, + block: ethereum.Block +): void { + const totalNumberOfActiveStreamsDelta = getActiveStreamsDelta( + isCreate, + isDelete + ); + const totalNumberOfClosedStreamsDelta = getClosedStreamsDelta(isDelete); const senderATS = getOrInitAccountTokenSnapshot( senderAddress, tokenAddress, @@ -1015,6 +1417,83 @@ export function updateAggregateEntitiesStreamData( senderATS.maybeCriticalAtTimestamp ); + if (isCFA) { + senderATS.totalCFANetFlowRate = + senderATS.totalCFANetFlowRate.minus(flowRateDelta); + + // the outflow rate should never go below 0. + senderATS.totalCFAOutflowRate = senderATS.totalCFAOutflowRate + .plus(flowRateDelta) + .lt(BIG_INT_ZERO) + ? newFlowRate + : senderATS.totalCFAOutflowRate.plus(flowRateDelta); + + senderATS.totalCFANumberOfActiveStreams = + senderATS.totalCFANumberOfActiveStreams + + totalNumberOfActiveStreamsDelta; + senderATS.activeCFAOutgoingStreamCount = + senderATS.activeCFAOutgoingStreamCount + + totalNumberOfActiveStreamsDelta; + senderATS.inactiveCFAOutgoingStreamCount = + senderATS.inactiveCFAOutgoingStreamCount + + totalNumberOfClosedStreamsDelta; + + senderATS.totalCFANumberOfClosedStreams = + senderATS.totalCFANumberOfClosedStreams + + totalNumberOfClosedStreamsDelta; + senderATS.totalCFADeposit = + senderATS.totalCFADeposit.plus(depositDelta); + } else { + senderATS.totalGDANetFlowRate = + senderATS.totalGDANetFlowRate.minus(flowRateDelta); + + // the outflow rate should never go below 0. + senderATS.totalGDAOutflowRate = senderATS.totalGDAOutflowRate + .plus(flowRateDelta) + .lt(BIG_INT_ZERO) + ? newFlowRate + : senderATS.totalGDAOutflowRate.plus(flowRateDelta); + + senderATS.totalGDANumberOfActiveStreams = + senderATS.totalGDANumberOfActiveStreams + + totalNumberOfActiveStreamsDelta; + senderATS.activeGDAOutgoingStreamCount = + senderATS.activeGDAOutgoingStreamCount + + totalNumberOfActiveStreamsDelta; + senderATS.inactiveGDAOutgoingStreamCount = + senderATS.inactiveGDAOutgoingStreamCount + + totalNumberOfClosedStreamsDelta; + + senderATS.totalGDANumberOfClosedStreams = + senderATS.totalGDANumberOfClosedStreams + + totalNumberOfClosedStreamsDelta; + senderATS.totalGDADeposit = + senderATS.totalGDADeposit.plus(depositDelta); + } + + senderATS.save(); +} + +/** + * Updates TokenStatistic and AccountTokenSnapshot countable stream + * data. Must be called after updating streamed amount data for the + * AccountTokenSnapshot entities. + */ +export function updateReceiverATSStreamData( + receiverAddress: Address, + tokenAddress: Address, + newFlowRate: BigInt, + flowRateDelta: BigInt, + isCreate: boolean, + isDelete: boolean, + isCFA: boolean, + block: ethereum.Block +): void { + const totalNumberOfActiveStreamsDelta = getActiveStreamsDelta( + isCreate, + isDelete + ); + const totalNumberOfClosedStreamsDelta = getClosedStreamsDelta(isDelete); const receiverATS = getOrInitAccountTokenSnapshot( receiverAddress, tokenAddress, @@ -1049,10 +1528,58 @@ export function updateAggregateEntitiesStreamData( receiverATS.totalNetFlowRate, receiverATS.maybeCriticalAtTimestamp ); - receiverATS.save(); - tokenStatistic.save(); - senderATS.save(); + if (isCFA) { + receiverATS.totalCFANetFlowRate = + receiverATS.totalCFANetFlowRate.plus(flowRateDelta); + + // the inflow rate should never go below 0. + receiverATS.totalCFAInflowRate = receiverATS.totalCFAInflowRate + .plus(flowRateDelta) + .lt(BIG_INT_ZERO) + ? newFlowRate + : receiverATS.totalCFAInflowRate.plus(flowRateDelta); + + receiverATS.totalCFANumberOfActiveStreams = + receiverATS.totalCFANumberOfActiveStreams + + totalNumberOfActiveStreamsDelta; + receiverATS.activeCFAIncomingStreamCount = + receiverATS.activeCFAIncomingStreamCount + + totalNumberOfActiveStreamsDelta; + receiverATS.inactiveCFAIncomingStreamCount = + receiverATS.inactiveCFAIncomingStreamCount + + totalNumberOfClosedStreamsDelta; + + receiverATS.totalCFANumberOfClosedStreams = + receiverATS.totalCFANumberOfClosedStreams + + totalNumberOfClosedStreamsDelta; + } else { + receiverATS.totalGDANetFlowRate = + receiverATS.totalGDANetFlowRate.plus(flowRateDelta); + + // the inflow rate should never go below 0. + receiverATS.totalGDAInflowRate = receiverATS.totalGDAInflowRate + .plus(flowRateDelta) + .lt(BIG_INT_ZERO) + ? newFlowRate + : receiverATS.totalGDAInflowRate.plus(flowRateDelta); + + receiverATS.totalGDANumberOfActiveStreams = + receiverATS.totalGDANumberOfActiveStreams + + totalNumberOfActiveStreamsDelta; + receiverATS.activeGDAIncomingStreamCount = + receiverATS.activeGDAIncomingStreamCount + + totalNumberOfActiveStreamsDelta; + receiverATS.inactiveGDAIncomingStreamCount = + receiverATS.inactiveGDAIncomingStreamCount + + totalNumberOfClosedStreamsDelta; + + receiverATS.totalGDANumberOfClosedStreams = + receiverATS.totalGDANumberOfClosedStreams + + totalNumberOfClosedStreamsDelta; + } + + receiverATS.save(); } export function updateAggregateEntitiesTransferData( diff --git a/packages/subgraph/src/mappings/cfav1.ts b/packages/subgraph/src/mappings/cfav1.ts index 4176441ce3..fa548d6091 100644 --- a/packages/subgraph/src/mappings/cfav1.ts +++ b/packages/subgraph/src/mappings/cfav1.ts @@ -29,8 +29,11 @@ import { getOrInitFlowOperator, getOrInitStream, getOrInitStreamRevision, - updateAggregateEntitiesStreamData, + updateSenderATSStreamData, + updateReceiverATSStreamData, updateATSStreamedAndBalanceUntilUpdatedAt, + updateTokenStatisticStreamData, + updateTokenStatsStreamedUntilUpdatedAt, } from "../mappingHelpers"; import { getHostAddress } from "../addresses"; @@ -78,7 +81,9 @@ export function handleFlowUpdated(event: FlowUpdated): void { const oldDeposit = stream.deposit; const oldFlowRate = stream.currentFlowRate; - const timeSinceLastUpdate = currentTimestamp.minus(stream.updatedAtTimestamp); + const timeSinceLastUpdate = currentTimestamp.minus( + stream.updatedAtTimestamp + ); const userAmountStreamedSinceLastUpdate = oldFlowRate.times(timeSinceLastUpdate); const newStreamedUntilLastUpdate = stream.streamedUntilUpdatedAt.plus( @@ -124,6 +129,7 @@ export function handleFlowUpdated(event: FlowUpdated): void { newDeposit ); + // update streamed and balance until updated at for sender and receiver updateATSStreamedAndBalanceUntilUpdatedAt( senderAddress, tokenAddress, @@ -137,19 +143,46 @@ export function handleFlowUpdated(event: FlowUpdated): void { event.block, null ); - // @note EXCEPTION for not calling updateTokenStatsStreamedUntilUpdatedAt - // because updateAggregateEntitiesStreamData updates tokenStats.streamedUntilUpdatedAt - updateAggregateEntitiesStreamData( + + // update stream counter data for sender and receiver ATS + updateSenderATSStreamData( senderAddress, + tokenAddress, + flowRate, + flowRateDelta, + depositDelta, + isCreate, + isDelete, + true, + event.block + ); + updateReceiverATSStreamData( receiverAddress, + tokenAddress, + flowRate, + flowRateDelta, + isCreate, + isDelete, + true, + event.block + ); + + // update token stats streamed until updated at + updateTokenStatsStreamedUntilUpdatedAt(tokenAddress, event.block); + + // update token stats stream counter data + updateTokenStatisticStreamData( tokenAddress, flowRate, flowRateDelta, depositDelta, isCreate, isDelete, + true, event.block ); + + // create ATS and token statistic log entities _createAccountTokenSnapshotLogEntity( event, senderAddress, diff --git a/packages/subgraph/src/mappings/gdav1.ts b/packages/subgraph/src/mappings/gdav1.ts new file mode 100644 index 0000000000..58f57be3ae --- /dev/null +++ b/packages/subgraph/src/mappings/gdav1.ts @@ -0,0 +1,490 @@ +import { BigInt } from "@graphprotocol/graph-ts"; +import { + BufferAdjusted, + FlowDistributionUpdated, + InstantDistributionUpdated, + PoolConnectionUpdated, + PoolCreated, +} from "../../generated/GeneralDistributionAgreementV1/IGeneralDistributionAgreementV1"; +import { + BufferAdjustedEvent, + FlowDistributionUpdatedEvent, + InstantDistributionUpdatedEvent, + PoolConnectionUpdatedEvent, + PoolCreatedEvent, +} from "../../generated/schema"; +import { SuperfluidPool as SuperfluidPoolTemplate } from "../../generated/templates"; +import { + _createAccountTokenSnapshotLogEntity, + _createTokenStatisticLogEntity, + getOrInitPool, + getOrInitPoolDistributor, + getOrInitPoolMember, + getOrInitTokenStatistic, + updateATSStreamedAndBalanceUntilUpdatedAt, + updateAggregateDistributionAgreementData, + updatePoolDistributorTotalAmountFlowedAndDistributed, + updatePoolTotalAmountFlowedAndDistributed, + updateSenderATSStreamData, + updateTokenStatisticStreamData, + updateTokenStatsStreamedUntilUpdatedAt, +} from "../mappingHelpers"; +import { + BIG_INT_ZERO, + createEventID, + initializeEventEntity, + membershipWithUnitsExists, +} from "../utils"; + +// @note use deltas where applicable + +export function handlePoolCreated(event: PoolCreated): void { + const eventName = "PoolCreated"; + + const pool = getOrInitPool(event, event.params.pool.toHex()); + pool.token = event.params.token.toHex(); + pool.admin = event.params.admin.toHex(); + pool.save(); + + // Note: this is necessary otherwise we will not be able to capture + // template data source events. + SuperfluidPoolTemplate.create(event.params.pool); + + updateTokenStatsStreamedUntilUpdatedAt(event.params.token, event.block); + + const tokenStatistic = getOrInitTokenStatistic( + event.params.token, + event.block + ); + tokenStatistic.totalNumberOfPools = tokenStatistic.totalNumberOfPools + 1; + + tokenStatistic.save(); + + updateATSStreamedAndBalanceUntilUpdatedAt( + event.params.admin, + event.params.token, + event.block, + null + ); + + _createAccountTokenSnapshotLogEntity( + event, + event.params.admin, + event.params.token, + eventName + ); + + _createTokenStatisticLogEntity(event, event.params.token, eventName); + // Create Event Entity + _createPoolCreatedEntity(event); +} + +export function handlePoolConnectionUpdated( + event: PoolConnectionUpdated +): void { + // Update Pool Member Entity + const poolMember = getOrInitPoolMember( + event, + event.params.pool, + event.params.account + ); + const previousIsConnected = poolMember.isConnected; + const memberConnectedStatusUpdated = + previousIsConnected !== event.params.connected; + poolMember.isConnected = event.params.connected; + poolMember.save(); + + const hasMembershipWithUnits = membershipWithUnitsExists(poolMember.id); + + // Update Pool Entity + let pool = getOrInitPool(event, event.params.pool.toHex()); + pool = updatePoolTotalAmountFlowedAndDistributed(event, pool); + if (poolMember.units.gt(BIG_INT_ZERO)) { + if (memberConnectedStatusUpdated) { + // disconnected -> connected case + if (event.params.connected) { + pool.totalConnectedUnits = pool.totalConnectedUnits.plus( + poolMember.units + ); + pool.totalDisconnectedUnits = pool.totalDisconnectedUnits.minus( + poolMember.units + ); + pool.totalConnectedMembers = pool.totalConnectedMembers + 1; + pool.totalDisconnectedMembers = + pool.totalDisconnectedMembers - 1; + } else { + // connected -> disconnected case + pool.totalConnectedUnits = pool.totalConnectedUnits.minus( + poolMember.units + ); + pool.totalDisconnectedUnits = pool.totalDisconnectedUnits.plus( + poolMember.units + ); + pool.totalConnectedMembers = pool.totalConnectedMembers - 1; + pool.totalDisconnectedMembers = + pool.totalDisconnectedMembers + 1; + } + } + } + pool.save(); + + // Update Token Stats Streamed Until Updated At + updateTokenStatsStreamedUntilUpdatedAt(event.params.token, event.block); + // Update ATS Balance and Streamed Until Updated At + updateATSStreamedAndBalanceUntilUpdatedAt( + event.params.account, + event.params.token, + event.block, + null + ); + + const isConnecting = event.params.connected; + + // there is no concept of revoking in GDA, but in the subgraph + // revoking is disconnecting and deleting is setting units to 0 + const isRevoking = !event.params.connected; + + updateAggregateDistributionAgreementData( + event.params.account, + event.params.token, + hasMembershipWithUnits || poolMember.isConnected, + poolMember.isConnected, + false, // don't increment memberWithUnits + isRevoking, // isRevoking + false, // not deleting (setting units to 0) + isConnecting, // approving membership here + event.block, + false // isIDA + ); + + // Create ATS and Token Statistic Log Entities + const eventName = "PoolConnectionUpdated"; + _createAccountTokenSnapshotLogEntity( + event, + event.params.account, + event.params.token, + eventName + ); + + _createTokenStatisticLogEntity(event, event.params.token, eventName); + + // Create Event Entity + _createPoolConnectionUpdatedEntity(event, poolMember.id); +} + +export function handleBufferAdjusted(event: BufferAdjusted): void { + // Update Pool Distributor + let poolDistributor = getOrInitPoolDistributor( + event, + event.params.pool, + event.params.from + ); + poolDistributor = updatePoolDistributorTotalAmountFlowedAndDistributed( + event, + poolDistributor + ); + poolDistributor.totalBuffer = event.params.newBufferAmount; + poolDistributor.save(); + + // Update Pool + let pool = getOrInitPool(event, event.params.pool.toHex()); + pool = updatePoolTotalAmountFlowedAndDistributed(event, pool); + pool.totalBuffer = pool.totalBuffer.plus(event.params.bufferDelta); + pool.save(); + + // Update Token Stats Buffer + const tokenStatistic = getOrInitTokenStatistic( + event.params.token, + event.block + ); + tokenStatistic.totalGDADeposit = tokenStatistic.totalGDADeposit.plus( + event.params.bufferDelta + ); + tokenStatistic.save(); + + // Create Event Entity + _createBufferAdjustedEntity(event, poolDistributor.id); +} + +export function handleFlowDistributionUpdated( + event: FlowDistributionUpdated +): void { + // Update Pool Distributor + let poolDistributor = getOrInitPoolDistributor( + event, + event.params.pool, + event.params.distributor + ); + poolDistributor = updatePoolDistributorTotalAmountFlowedAndDistributed( + event, + poolDistributor + ); + poolDistributor.flowRate = event.params.newDistributorToPoolFlowRate; + poolDistributor.save(); + + // Update Pool + let pool = getOrInitPool(event, event.params.pool.toHex()); + pool = updatePoolTotalAmountFlowedAndDistributed(event, pool); + pool.flowRate = event.params.newTotalDistributionFlowRate; + pool.adjustmentFlowRate = event.params.adjustmentFlowRate; + pool.save(); + + const flowRateDelta = event.params.newDistributorToPoolFlowRate.minus( + event.params.oldFlowRate + ); + + const isCreate = event.params.oldFlowRate.equals(BIG_INT_ZERO); + const isDelete = + event.params.newDistributorToPoolFlowRate.equals(BIG_INT_ZERO); + + // Update Token Statistics + const eventName = "FlowDistributionUpdated"; + updateTokenStatsStreamedUntilUpdatedAt(event.params.token, event.block); + _createTokenStatisticLogEntity(event, event.params.token, eventName); + updateTokenStatisticStreamData( + event.params.token, + event.params.newDistributorToPoolFlowRate, + flowRateDelta, + BIG_INT_ZERO, + isCreate, + isDelete, + false, + event.block + ); + _createTokenStatisticLogEntity(event, event.params.token, eventName); + + // Update ATS + updateSenderATSStreamData( + event.params.distributor, + event.params.token, + event.params.newDistributorToPoolFlowRate, + flowRateDelta, + BIG_INT_ZERO, + isCreate, + isDelete, + false, + event.block + ); + updateATSStreamedAndBalanceUntilUpdatedAt( + event.params.distributor, + event.params.token, + event.block, + null + ); + _createAccountTokenSnapshotLogEntity( + event, + event.params.distributor, + event.params.token, + eventName + ); + + // Create Event Entity + _createFlowDistributionUpdatedEntity(event, poolDistributor.id, pool.totalUnits); +} + +export function handleInstantDistributionUpdated( + event: InstantDistributionUpdated +): void { + // Update Pool Distributor + let poolDistributor = getOrInitPoolDistributor( + event, + event.params.pool, + event.params.distributor + ); + poolDistributor = updatePoolDistributorTotalAmountFlowedAndDistributed( + event, + poolDistributor + ); + poolDistributor.totalAmountInstantlyDistributedUntilUpdatedAt = + poolDistributor.totalAmountInstantlyDistributedUntilUpdatedAt.plus( + event.params.actualAmount + ); + poolDistributor.totalAmountDistributedUntilUpdatedAt = + poolDistributor.totalAmountDistributedUntilUpdatedAt.plus( + event.params.actualAmount + ); + poolDistributor.save(); + + // Update Pool + let pool = getOrInitPool(event, event.params.pool.toHex()); + pool = updatePoolTotalAmountFlowedAndDistributed(event, pool); + const previousTotalAmountDistributed = + pool.totalAmountDistributedUntilUpdatedAt; + pool.totalAmountInstantlyDistributedUntilUpdatedAt = + pool.totalAmountInstantlyDistributedUntilUpdatedAt.plus( + event.params.actualAmount + ); + pool.totalAmountDistributedUntilUpdatedAt = + pool.totalAmountDistributedUntilUpdatedAt.plus( + event.params.actualAmount + ); + pool.save(); + + // Update Token Statistic + const tokenStatistic = getOrInitTokenStatistic( + event.params.token, + event.block + ); + + if (previousTotalAmountDistributed.equals(BIG_INT_ZERO)) { + tokenStatistic.totalNumberOfActivePools = + tokenStatistic.totalNumberOfActivePools + 1; + } + + tokenStatistic.totalAmountDistributedUntilUpdatedAt = + tokenStatistic.totalAmountDistributedUntilUpdatedAt.plus( + event.params.actualAmount + ); + tokenStatistic.save(); + + const eventName = "InstantDistributionUpdated"; + updateTokenStatsStreamedUntilUpdatedAt(event.params.token, event.block); + _createTokenStatisticLogEntity(event, event.params.token, eventName); + + // Update ATS + updateATSStreamedAndBalanceUntilUpdatedAt( + event.params.distributor, + event.params.token, + event.block, + null + ); + + _createAccountTokenSnapshotLogEntity( + event, + event.params.distributor, + event.params.token, + eventName + ); + + // Create Event Entity + _createInstantDistributionUpdatedEntity(event, poolDistributor.id, pool.totalUnits); +} + +// Event Entity Creation Functions + +function _createPoolCreatedEntity(event: PoolCreated): PoolCreatedEvent { + const ev = new PoolCreatedEvent(createEventID("PoolCreated", event)); + initializeEventEntity(ev, event, [ + event.params.token, + event.params.pool, + event.transaction.from, + event.params.admin, + ]); + + ev.token = event.params.token; + ev.caller = event.transaction.from; + ev.admin = event.params.admin; + ev.pool = event.params.pool.toHex(); + + ev.save(); + + return ev; +} + +function _createPoolConnectionUpdatedEntity( + event: PoolConnectionUpdated, + poolMemberId: string +): PoolConnectionUpdatedEvent { + const ev = new PoolConnectionUpdatedEvent( + createEventID("PoolConnectionUpdated", event) + ); + initializeEventEntity(ev, event, [ + event.params.token, + event.params.pool, + event.params.account, + ]); + + ev.token = event.params.token; + ev.connected = event.params.connected; + ev.pool = event.params.pool.toHex(); + ev.poolMember = poolMemberId; + ev.userData = event.params.userData; + + ev.save(); + + return ev; +} + +function _createBufferAdjustedEntity( + event: BufferAdjusted, + poolDistributorId: string +): BufferAdjustedEvent { + const ev = new BufferAdjustedEvent(createEventID("BufferAdjusted", event)); + initializeEventEntity(ev, event, [ + event.params.token, + event.params.pool, + event.params.from, + ]); + + ev.token = event.params.token; + ev.bufferDelta = event.params.bufferDelta; + ev.newBufferAmount = event.params.newBufferAmount; + ev.totalBufferAmount = event.params.totalBufferAmount; + ev.pool = event.params.pool.toHex(); + ev.poolDistributor = poolDistributorId; + + ev.save(); + + return ev; +} + +function _createInstantDistributionUpdatedEntity( + event: InstantDistributionUpdated, + poolDistributorId: string, + totalUnits: BigInt +): InstantDistributionUpdatedEvent { + const ev = new InstantDistributionUpdatedEvent( + createEventID("InstantDistributionUpdated", event) + ); + initializeEventEntity(ev, event, [ + event.params.token, + event.params.pool, + event.params.distributor, + event.params.operator, + ]); + + ev.token = event.params.token; + ev.operator = event.params.operator; + ev.requestedAmount = event.params.requestedAmount; + ev.actualAmount = event.params.actualAmount; + ev.pool = event.params.pool.toHex(); + ev.poolDistributor = poolDistributorId; + ev.totalUnits = totalUnits; + ev.userData = event.params.userData; + + ev.save(); + + return ev; +} + +function _createFlowDistributionUpdatedEntity( + event: FlowDistributionUpdated, + poolDistributorId: string, + totalUnits: BigInt +): FlowDistributionUpdatedEvent { + const ev = new FlowDistributionUpdatedEvent( + createEventID("FlowDistributionUpdated", event) + ); + initializeEventEntity(ev, event, [ + event.params.token, + event.params.pool, + event.params.distributor, + event.params.operator, + ]); + + ev.token = event.params.token; + ev.operator = event.params.operator; + ev.oldFlowRate = event.params.oldFlowRate; + ev.newDistributorToPoolFlowRate = event.params.newDistributorToPoolFlowRate; + ev.newTotalDistributionFlowRate = event.params.newTotalDistributionFlowRate; + ev.adjustmentFlowRecipient = event.params.adjustmentFlowRecipient; + ev.adjustmentFlowRate = event.params.adjustmentFlowRate; + ev.pool = event.params.pool.toHex(); + ev.poolDistributor = poolDistributorId; + ev.totalUnits = totalUnits; + ev.userData = event.params.userData; + + ev.save(); + + return ev; +} diff --git a/packages/subgraph/src/mappings/idav1.ts b/packages/subgraph/src/mappings/idav1.ts index 3ab12b8111..5ebcf73d4d 100644 --- a/packages/subgraph/src/mappings/idav1.ts +++ b/packages/subgraph/src/mappings/idav1.ts @@ -28,7 +28,7 @@ import { createEventID, getIndexID, initializeEventEntity, - subscriptionExists as subscriptionWithUnitsExists, + subscriptionWithUnitsExists, tokenHasValidHost, } from "../utils"; import { @@ -37,7 +37,7 @@ import { getOrInitIndex, getOrInitSubscription, getOrInitTokenStatistic, - updateAggregateIDASubscriptionsData, + updateAggregateDistributionAgreementData, updateATSStreamedAndBalanceUntilUpdatedAt, updateTokenStatsStreamedUntilUpdatedAt, } from "../mappingHelpers"; @@ -54,7 +54,6 @@ export function handleIndexCreated(event: IndexCreated): void { return; } - const currentTimestamp = event.block.timestamp; const indexCreatedId = createEventID(eventName, event); const index = getOrInitIndex( event, @@ -75,8 +74,7 @@ export function handleIndexCreated(event: IndexCreated): void { ); tokenStatistic.totalNumberOfIndexes = tokenStatistic.totalNumberOfIndexes + 1; - tokenStatistic.updatedAtTimestamp = currentTimestamp; - tokenStatistic.updatedAtBlockNumber = event.block.number; + tokenStatistic.save(); updateATSStreamedAndBalanceUntilUpdatedAt( @@ -297,16 +295,17 @@ export function handleSubscriptionApproved(event: SubscriptionApproved): void { updateTokenStatsStreamedUntilUpdatedAt(event.params.token, event.block); // we only want to increment approved here ALWAYS - updateAggregateIDASubscriptionsData( + updateAggregateDistributionAgreementData( event.params.subscriber, event.params.token, hasSubscriptionWithUnits || subscription.approved, subscription.approved, false, // don't increment subWithUnits false, // not revoking - false, // not deleting + false, // not deleting (setting units to 0) true, // approving subscription here - event.block + event.block, + true // isIDA ); index.save(); @@ -451,7 +450,7 @@ export function handleSubscriptionRevoked(event: SubscriptionRevoked): void { updateTokenStatsStreamedUntilUpdatedAt(event.params.token, event.block); - updateAggregateIDASubscriptionsData( + updateAggregateDistributionAgreementData( event.params.subscriber, event.params.token, true, @@ -460,7 +459,8 @@ export function handleSubscriptionRevoked(event: SubscriptionRevoked): void { true, // revoking subscription here false, // not deleting false, // not approving - event.block + event.block, + true // isIDA ); // mimic ida logic more closely updateATSStreamedAndBalanceUntilUpdatedAt( @@ -578,7 +578,7 @@ export function handleSubscriptionUnitsUpdated( // and therefore subtracts the number of totalSubscriptionWithUnits and // totalApprovedSubscriptions if (units.equals(BIG_INT_ZERO)) { - updateAggregateIDASubscriptionsData( + updateAggregateDistributionAgreementData( event.params.subscriber, event.params.token, hasSubscriptionWithUnits, @@ -587,7 +587,8 @@ export function handleSubscriptionUnitsUpdated( false, // not revoking subscription true, // only place we decrement subWithUnits IF subscriber has subWithUnits false, // not approving - event.block + event.block, + true // isIDA ); index.totalSubscriptionsWithUnits = hasSubscriptionWithUnits ? index.totalSubscriptionsWithUnits - 1 @@ -604,7 +605,7 @@ export function handleSubscriptionUnitsUpdated( index.totalSubscriptionsWithUnits = index.totalSubscriptionsWithUnits + 1; - updateAggregateIDASubscriptionsData( + updateAggregateDistributionAgreementData( event.params.subscriber, event.params.token, hasSubscriptionWithUnits, @@ -613,7 +614,8 @@ export function handleSubscriptionUnitsUpdated( false, // not revoking false, // not deleting false, // not approving - event.block + event.block, + true // isIDA ); } diff --git a/packages/subgraph/src/mappings/superfluidPool.ts b/packages/subgraph/src/mappings/superfluidPool.ts new file mode 100644 index 0000000000..cdc3e5eb6d --- /dev/null +++ b/packages/subgraph/src/mappings/superfluidPool.ts @@ -0,0 +1,162 @@ +import { BigInt } from "@graphprotocol/graph-ts"; +import { + DistributionClaimed, + MemberUnitsUpdated, +} from "../../generated/GeneralDistributionAgreementV1/ISuperfluidPool"; +import { DistributionClaimedEvent, MemberUnitsUpdatedEvent } from "../../generated/schema"; +import { + _createAccountTokenSnapshotLogEntity, + _createTokenStatisticLogEntity, + getOrInitPool, + getOrInitPoolMember, + updateATSStreamedAndBalanceUntilUpdatedAt, + updateAggregateDistributionAgreementData, + updatePoolTotalAmountFlowedAndDistributed, + updateTokenStatsStreamedUntilUpdatedAt, +} from "../mappingHelpers"; +import { BIG_INT_ZERO, createEventID, initializeEventEntity, membershipWithUnitsExists } from "../utils"; + +// @note use deltas where applicable + +export function handleDistributionClaimed(event: DistributionClaimed): void { + const token = event.params.token; + + // Update Pool + let pool = getOrInitPool(event, event.address.toHex()); + pool = updatePoolTotalAmountFlowedAndDistributed(event, pool); + pool.save(); + + // Update PoolMember + const poolMember = getOrInitPoolMember(event, event.address, event.params.member); + poolMember.totalAmountClaimed = event.params.totalClaimed; + poolMember.save(); + + // Update Token Statistics + const eventName = "DistributionClaimed"; + updateTokenStatsStreamedUntilUpdatedAt(token, event.block); + _createTokenStatisticLogEntity(event, token, eventName); + + // Update ATS + updateATSStreamedAndBalanceUntilUpdatedAt(event.params.member, token, event.block, null); + _createAccountTokenSnapshotLogEntity(event, event.params.member, token, eventName); + + // Create Event Entity + _createDistributionClaimedEntity(event, poolMember.id); +} + +export function handleMemberUnitsUpdated(event: MemberUnitsUpdated): void { + // - PoolMember + // - units + const poolMember = getOrInitPoolMember(event, event.address, event.params.member); + const hasMembershipWithUnits = membershipWithUnitsExists(poolMember.id); + + const previousUnits = poolMember.units; + const unitsDelta = event.params.newUnits.minus(previousUnits); + poolMember.units = event.params.newUnits; + + poolMember.save(); + + const eventName = "MemberUnitsUpdated"; + updateTokenStatsStreamedUntilUpdatedAt(event.params.token, event.block); + _createTokenStatisticLogEntity(event, event.params.token, eventName); + + updateATSStreamedAndBalanceUntilUpdatedAt(event.params.member, event.params.token, event.block, null); + _createAccountTokenSnapshotLogEntity(event, event.params.member, event.params.token, eventName); + + let pool = getOrInitPool(event, event.address.toHex()); + pool = updatePoolTotalAmountFlowedAndDistributed(event, pool); + if (poolMember.isConnected) { + pool.totalConnectedUnits = pool.totalConnectedUnits.plus(unitsDelta); + } else { + pool.totalDisconnectedUnits = pool.totalDisconnectedUnits.plus(unitsDelta); + } + pool.totalUnits = pool.totalUnits.plus(unitsDelta); + pool.save(); + + // 0 units to > 0 units + if (previousUnits.equals(BIG_INT_ZERO) && event.params.newUnits.gt(BIG_INT_ZERO)) { + pool.totalMembers = pool.totalMembers + 1; + // if the member is connected with units now, we add one to connected + if (poolMember.isConnected) { + pool.totalConnectedMembers = pool.totalConnectedMembers + 1; + } else { + // if the member is disconnected with units now, we add one to disconnected + pool.totalDisconnectedMembers = pool.totalDisconnectedMembers + 1; + } + pool.save(); + + updateAggregateDistributionAgreementData( + event.params.member, + event.params.token, + hasMembershipWithUnits, + poolMember.isConnected, + true, // only place we increment subWithUnits + false, // not deleting + false, // not deleting + false, // not connecting + event.block, + false // isIDA + ); + } + // > 0 units to 0 units + if (previousUnits.gt(BIG_INT_ZERO) && poolMember.units.equals(BIG_INT_ZERO)) { + pool.totalMembers = pool.totalMembers - 1; + // if the member is connected with no units now, we subtract one from connected + if (poolMember.isConnected) { + pool.totalConnectedMembers = pool.totalConnectedMembers - 1; + } else { + // if the member is disconnected with no units now, we subtract one from disconnected + pool.totalDisconnectedMembers = pool.totalDisconnectedMembers - 1; + } + pool.save(); + + updateAggregateDistributionAgreementData( + event.params.member, + event.params.token, + hasMembershipWithUnits, + poolMember.isConnected, + false, // don't increment memberWithUnits + false, // not disconnecting membership + true, // only place we decrement membershipWithUnits IF member has memberShipWithUnits + false, // not connecting + event.block, + false // isIDA + ); + } + + // Create Event Entity + _createMemberUnitsUpdatedEntity(event, poolMember.id, pool.totalUnits); +} + +function _createDistributionClaimedEntity(event: DistributionClaimed, poolMemberId: string): DistributionClaimedEvent { + const ev = new DistributionClaimedEvent(createEventID("DistributionClaimed", event)); + initializeEventEntity(ev, event, [event.params.token, event.address, event.params.member]); + + ev.token = event.params.token; + ev.claimedAmount = event.params.claimedAmount; + ev.totalClaimed = event.params.totalClaimed; + ev.pool = event.address.toHex(); + ev.poolMember = poolMemberId; + ev.save(); + + return ev; +} + +function _createMemberUnitsUpdatedEntity( + event: MemberUnitsUpdated, + poolMemberId: string, + totalUnits: BigInt +): MemberUnitsUpdatedEvent { + const ev = new MemberUnitsUpdatedEvent(createEventID("MemberUnitsUpdated", event)); + initializeEventEntity(ev, event, [event.params.token, event.address, event.params.member]); + + ev.token = event.params.token; + ev.oldUnits = event.params.oldUnits; + ev.units = event.params.newUnits; + ev.totalUnits = totalUnits; + ev.pool = event.address.toHex(); + ev.poolMember = poolMemberId; + ev.save(); + + return ev; +} diff --git a/packages/subgraph/src/utils.ts b/packages/subgraph/src/utils.ts index 7f8373a9cc..1d11509bc2 100644 --- a/packages/subgraph/src/utils.ts +++ b/packages/subgraph/src/utils.ts @@ -1,10 +1,19 @@ -import { Address, BigInt, Bytes, crypto, Entity, ethereum, log, Value } from "@graphprotocol/graph-ts"; +import { + Address, + BigInt, + Bytes, + crypto, + Entity, + ethereum, + log, + Value, +} from "@graphprotocol/graph-ts"; import { ISuperToken as SuperToken } from "../generated/templates/SuperToken/ISuperToken"; -import { Resolver } from "../generated/ResolverV1/Resolver"; import { IndexSubscription, Token, TokenStatistic, + PoolMember, } from "../generated/schema"; /************************************************************************** @@ -29,7 +38,7 @@ export function bytesToAddress(bytes: Bytes): Address { * @param values * @returns the encoded bytes */ - export function encode(values: Array): Bytes { +export function encode(values: Array): Bytes { return ethereum.encode( // forcefully cast Value[] -> Tuple ethereum.Value.fromTuple(changetype(values)) @@ -63,7 +72,7 @@ export function initializeEventEntity( entity: Entity, event: ethereum.Event, addresses: Bytes[] - ): Entity { +): Entity { const idValue = entity.get("id"); if (!idValue) return entity; @@ -72,7 +81,10 @@ export function initializeEventEntity( entity.set("blockNumber", Value.fromBigInt(event.block.number)); entity.set("logIndex", Value.fromBigInt(event.logIndex)); - entity.set("order", Value.fromBigInt(getOrder(event.block.number, event.logIndex))); + entity.set( + "order", + Value.fromBigInt(getOrder(event.block.number, event.logIndex)) + ); entity.set("name", Value.fromString(name)); entity.set("addresses", Value.fromBytesArray(addresses)); entity.set("timestamp", Value.fromBigInt(event.block.timestamp)); @@ -82,7 +94,7 @@ export function initializeEventEntity( if (receipt) { entity.set("gasUsed", Value.fromBigInt(receipt.gasUsed)); } else { - // @note `gasUsed` is a non-nullable property in our `schema.graphql` file, so when we attempt to save + // @note `gasUsed` is a non-nullable property in our `schema.graphql` file, so when we attempt to save // the entity with a null field, it will halt the subgraph indexing. // Nonetheless, we explicitly throw if receipt is null, as this can arise due forgetting to include // `receipt: true` under `eventHandlers` in our manifest (`subgraph.template.yaml`) file. @@ -90,7 +102,7 @@ export function initializeEventEntity( } return entity; - } +} /************************************************************************** * HOL entities util functions @@ -127,8 +139,8 @@ export function getTokenInfoAndReturn(token: Token): Token { /** * Gets and sets the total supply for TokenStatistic of a SuperToken upon initial creation - * @param tokenStatistic - * @param tokenAddress + * @param tokenStatistic + * @param tokenAddress * @returns TokenStatistic */ export function getInitialTotalSupplyForSuperToken( @@ -183,11 +195,7 @@ export function getStreamRevisionID( ethereum.Value.fromAddress(receiverAddress), ]; const flowId = crypto.keccak256(encode(values)); - return ( - flowId.toHex() + - "-" + - tokenAddress.toHex() - ); + return flowId.toHex() + "-" + tokenAddress.toHex(); } export function getStreamID( @@ -260,6 +268,27 @@ export function getIndexID( ); } +export function getPoolMemberID( + poolAddress: Address, + poolMemberAddress: Address +): string { + return ( + "poolMember-" + poolAddress.toHex() + "-" + poolMemberAddress.toHex() + ); +} + +export function getPoolDistributorID( + poolAddress: Address, + poolDistributorAddress: Address +): string { + return ( + "poolDistributor-" + + poolAddress.toHex() + + "-" + + poolDistributorAddress.toHex() + ); +} + // Get Aggregate ID functions export function getAccountTokenSnapshotID( accountAddress: Address, @@ -278,11 +307,24 @@ export function getAccountTokenSnapshotID( * @param id * @returns */ -export function subscriptionExists(id: string): boolean { +export function subscriptionWithUnitsExists(id: string): boolean { const subscription = IndexSubscription.load(id); return subscription != null && subscription.units.gt(BIG_INT_ZERO); } +/** + * If your units get set to 0, you will still have a pool member + * entity, but your pool member technically no longer exists. + * Similarly, you may be approved, but the pool member by this + * definition does not exist. + * @param id + * @returns + */ +export function membershipWithUnitsExists(id: string): boolean { + const poolMembership = PoolMember.load(id); + return poolMembership != null && poolMembership.units.gt(BIG_INT_ZERO); +} + export function getAmountStreamedSinceLastUpdatedAt( currentTime: BigInt, lastUpdatedTime: BigInt, @@ -292,6 +334,17 @@ export function getAmountStreamedSinceLastUpdatedAt( return timeDelta.times(flowRate); } +export function getActiveStreamsDelta( + isCreate: boolean, + isDelete: boolean +): i32 { + return isCreate ? 1 : isDelete ? -1 : 0; +} + +export function getClosedStreamsDelta(isDelete: boolean): i32 { + return isDelete ? 1 : 0; +} + /** * calculateMaybeCriticalAtTimestamp will return optimistic date based on updatedAtTimestamp, balanceUntilUpdatedAt and totalNetFlowRate. * @param updatedAtTimestamp diff --git a/packages/subgraph/subgraph.template.yaml b/packages/subgraph/subgraph.template.yaml index 7abe8db0d4..ab144634cf 100644 --- a/packages/subgraph/subgraph.template.yaml +++ b/packages/subgraph/subgraph.template.yaml @@ -102,6 +102,7 @@ dataSources: entities: - Account - AccountTokenSnapshot + - AccountTokenSnapshotLog - FlowOperator - FlowOperatorUpdatedEvent - FlowUpdatedEvent @@ -109,6 +110,7 @@ dataSources: - StreamPeriod - StreamRevision - TokenStatistic + - TokenStatisticLog abis: - name: IConstantFlowAgreementV1 file: ./abis/IConstantFlowAgreementV1.json @@ -143,6 +145,7 @@ dataSources: entities: - Account - AccountTokenSnapshot + - AccountTokenSnapshotLog - Index - IndexCreatedEvent - IndexDistributionClaimedEvent @@ -153,6 +156,7 @@ dataSources: - IndexUnsubscribedEvent - Token - TokenStatistic + - TokenStatisticLog - SubscriptionApprovedEvent - SubscriptionDistributionClaimedEvent - SubscriptionRevokedEvent @@ -197,6 +201,59 @@ dataSources: - event: SubscriptionUnitsUpdated(indexed address,indexed address,address,uint32,uint128,bytes) handler: handleSubscriptionUnitsUpdated receipt: true + - kind: ethereum/contract + name: GeneralDistributionAgreementV1 + network: {{ network }} + source: + address: "{{ gdaAddress }}" + abi: IGeneralDistributionAgreementV1 + startBlock: {{ hostStartBlock }} + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + file: ./src/mappings/gdav1.ts + entities: + - Account + - AccountTokenSnapshot + - AccountTokenSnapshotLog + - FlowDistributionUpdatedEvent + - InstantDistributionUpdatedEvent + - Pool + - PoolConnectionUpdatedEvent + - PoolCreatedEvent + - PoolDistributor + - PoolMember + - Token + - TokenStatistic + - TokenStatisticLog + abis: + - name: IGeneralDistributionAgreementV1 + file: ./abis/IGeneralDistributionAgreementV1.json + - name: ISuperfluidPool + file: ./abis/ISuperfluidPool.json + - name: ISuperToken + file: ./abis/ISuperToken.json + - name: Resolver + file: ./abis/Resolver.json + - name: ISuperfluid + file: ./abis/ISuperfluid.json + eventHandlers: + - event: BufferAdjusted(indexed address,indexed address,indexed address,int256,uint256,uint256) + handler: handleBufferAdjusted + receipt: true + - event: FlowDistributionUpdated(indexed address,indexed address,indexed address,address,int96,int96,int96,address,int96,bytes) + handler: handleFlowDistributionUpdated + receipt: true + - event: InstantDistributionUpdated(indexed address,indexed address,indexed address,address,uint256,uint256,bytes) + handler: handleInstantDistributionUpdated + receipt: true + - event: PoolConnectionUpdated(indexed address,indexed address,indexed address,bool,bytes) + handler: handlePoolConnectionUpdated + receipt: true + - event: PoolCreated(indexed address,indexed address,address) + handler: handlePoolCreated + receipt: true - kind: ethereum/contract name: ResolverV1 network: {{ network }} @@ -424,4 +481,41 @@ templates: receipt: true - event: BondIncreased(indexed address,uint256) handler: handleBondIncreased - receipt: true \ No newline at end of file + receipt: true + - kind: ethereum/contract + name: SuperfluidPool + network: {{ network }} + source: + abi: ISuperfluidPool + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + file: ./src/mappings/superfluidPool.ts + entities: + - Account + - AccountTokenSnapshot + - AccountTokenSnapshotLog + - DistributionClaimedEvent + - MemberUnitsUpdatedEvent + - Pool + - PoolMember + - Token + - TokenStatistic + - TokenStatisticLog + abis: + - name: ISuperfluidPool + file: ./abis/ISuperfluidPool.json + - name: ISuperToken + file: ./abis/ISuperToken.json + - name: Resolver + file: ./abis/Resolver.json + - name: ISuperfluid + file: ./abis/ISuperfluid.json + eventHandlers: + - event: MemberUnitsUpdated(indexed address,indexed address,uint128,uint128) + handler: handleMemberUnitsUpdated + receipt: true + - event: DistributionClaimed(indexed address,indexed address,int256,int256) + handler: handleDistributionClaimed + receipt: true diff --git a/packages/subgraph/tests/assertionHelpers.ts b/packages/subgraph/tests/assertionHelpers.ts index 572bd9080a..d844ead10d 100644 --- a/packages/subgraph/tests/assertionHelpers.ts +++ b/packages/subgraph/tests/assertionHelpers.ts @@ -1,6 +1,6 @@ import { Address, BigInt, ethereum } from "@graphprotocol/graph-ts"; import { assert, log } from "matchstick-as/assembly/index"; -import { createEventID, createLogID, getIndexID, getOrder } from "../src/utils"; +import { BIG_INT_ZERO, createEventID, createLogID, getIndexID, getOrder } from "../src/utils"; // General Assertion Helpers @@ -45,14 +45,11 @@ export function assertEventBaseProperties( export function assertHigherOrderBaseProperties( entityName: string, id: string, - createdAtTimestamp: BigInt, - createdAtBlockNumber: BigInt, - updatedAtTimestamp: BigInt, - updatedAtBlockNumber: BigInt + event: ethereum.Event, ): void { - assertAggregateBaseProperties(entityName, id, updatedAtTimestamp, updatedAtBlockNumber); - assert.fieldEquals(entityName, id, "createdAtTimestamp", createdAtTimestamp.toString()); - assert.fieldEquals(entityName, id, "createdAtBlockNumber", createdAtBlockNumber.toString()); + assertAggregateBaseProperties(entityName, id, event.block.timestamp, event.block.number); + assert.fieldEquals(entityName, id, "createdAtTimestamp", event.block.timestamp.toString()); + assert.fieldEquals(entityName, id, "createdAtBlockNumber", event.block.number.toString()); } /** @@ -145,15 +142,25 @@ export function assertIDAEventBaseProperties( * @param triggeredByEventName if triggeredByEventName is passed, we validate TokenStatisticLog * @param updatedAtTimestamp timestamp retrieved from the event * @param updatedAtBlockNumber block number retrieved from the event - * @param totalNumberOfActiveStreams expected count of active streams for the token - * @param totalNumberOfClosedStreams expected count of closed streams for the token + * @param totalNumberOfActiveStreams expected count of active streams for the token for all flow agreements + * @param totalCFANumberOfActiveStreams expected count of active streams for the token for the CFA + * @param totalGDANumberOfActiveStreams expected count of active streams for the token for the GDA + * @param totalNumberOfClosedStreams expected count of closed streams for the token for all flow agreements + * @param totalNumberOfCFAClosedStreams expected count of closed streams for the token for the CFA + * @param totalNumberOfGDAClosedStreams expected count of closed streams for the token for the GDA * @param totalNumberOfIndexes expected count of indexes for the token * @param totalNumberOfActiveIndexes expected count of active indexes for the token * @param totalSubscriptionsWithUnits expected count of subscriptions with allocated units for the token * @param totalApprovedSubscriptions expected totalNumber of approved subscriptions for the token - * @param totalDeposit expected total deposit amount - * @param totalOutflowRate expected total outflow rate - * @param totalAmountStreamedUntilUpdatedAt expected total amount streamed until updated at timestamp + * @param totalDeposit expected total deposit amount for all flow agreements + * @param totalCFADeposit expected total deposit amount for the CFA + * @param totalGDADeposit expected total deposit amount for the GDA + * @param totalOutflowRate expected total outflow rate for all flow agreements + * @param totalCFAOutflowRate expected total outflow rate for the CFA + * @param totalGDAOutflowRate expected total outflow rate for the GDA + * @param totalAmountStreamedUntilUpdatedAt expected total amount streamed until updated at timestamp for all flow agreements + * @param totalCFAAmountStreamedUntilUpdatedAt expected total amount streamed until updated at timestamp for the CFA + * @param totalGDAAmountStreamedUntilUpdatedAt expected total amount streamed until updated at timestamp for the GDA * @param totalAmountTransferredUntilUpdatedAt expected total amount transferred until updated at timestamp * @param totalAmountDistributedUntilUpdatedAt expected total amount distributed (with IDA) until updated at timestamp * @param totalSupply expected total supply @@ -167,14 +174,24 @@ export function assertTokenStatisticProperties( updatedAtTimestamp: BigInt, updatedAtBlockNumber: BigInt, totalNumberOfActiveStreams: i32, + totalCFANumberOfActiveStreams: i32, + totalGDANumberOfActiveStreams: i32, totalNumberOfClosedStreams: i32, + totalCFANumberOfClosedStreams: i32, + totalGDANumberOfClosedStreams: i32, totalNumberOfIndexes: i32, totalNumberOfActiveIndexes: i32, totalSubscriptionsWithUnits: i32, totalApprovedSubscriptions: i32, totalDeposit: BigInt, + totalCFADeposit: BigInt, + totalGDADeposit: BigInt, totalOutflowRate: BigInt, + totalCFAOutflowRate: BigInt, + totalGDAOutflowRate: BigInt, totalAmountStreamedUntilUpdatedAt: BigInt, + totalCFAAmountStreamedUntilUpdatedAt: BigInt, + totalGDAAmountStreamedUntilUpdatedAt: BigInt, totalAmountTransferredUntilUpdatedAt: BigInt, totalAmountDistributedUntilUpdatedAt: BigInt, totalSupply: BigInt, @@ -188,14 +205,24 @@ export function assertTokenStatisticProperties( const entityName = "TokenStatistic"; assertAggregateBaseProperties(entityName, id, updatedAtTimestamp, updatedAtBlockNumber); assert.fieldEquals(entityName, id, "totalNumberOfActiveStreams", totalNumberOfActiveStreams.toString()); + assert.fieldEquals(entityName, id, "totalCFANumberOfActiveStreams", totalCFANumberOfActiveStreams.toString()); + assert.fieldEquals(entityName, id, "totalGDANumberOfActiveStreams", totalGDANumberOfActiveStreams.toString()); assert.fieldEquals(entityName, id, "totalNumberOfClosedStreams", totalNumberOfClosedStreams.toString()); + assert.fieldEquals(entityName, id, "totalCFANumberOfClosedStreams", totalCFANumberOfClosedStreams.toString()); + assert.fieldEquals(entityName, id, "totalGDANumberOfClosedStreams", totalGDANumberOfClosedStreams.toString()); assert.fieldEquals(entityName, id, "totalNumberOfIndexes", totalNumberOfIndexes.toString()); assert.fieldEquals(entityName, id, "totalNumberOfActiveIndexes", totalNumberOfActiveIndexes.toString()); assert.fieldEquals(entityName, id, "totalSubscriptionsWithUnits", totalSubscriptionsWithUnits.toString()); assert.fieldEquals(entityName, id, "totalApprovedSubscriptions", totalApprovedSubscriptions.toString()); assert.fieldEquals(entityName, id, "totalDeposit", totalDeposit.toString()); + assert.fieldEquals(entityName, id, "totalCFADeposit", totalCFADeposit.toString()); + assert.fieldEquals(entityName, id, "totalGDADeposit", totalGDADeposit.toString()); assert.fieldEquals(entityName, id, "totalOutflowRate", totalOutflowRate.toString()); + assert.fieldEquals(entityName, id, "totalCFAOutflowRate", totalCFAOutflowRate.toString()); + assert.fieldEquals(entityName, id, "totalGDAOutflowRate", totalGDAOutflowRate.toString()); assert.fieldEquals(entityName, id, "totalAmountStreamedUntilUpdatedAt", totalAmountStreamedUntilUpdatedAt.toString()); + assert.fieldEquals(entityName, id, "totalCFAAmountStreamedUntilUpdatedAt", totalCFAAmountStreamedUntilUpdatedAt.toString()); + assert.fieldEquals(entityName, id, "totalGDAAmountStreamedUntilUpdatedAt", totalGDAAmountStreamedUntilUpdatedAt.toString()); assert.fieldEquals(entityName, id, "totalAmountTransferredUntilUpdatedAt", totalAmountTransferredUntilUpdatedAt.toString()); assert.fieldEquals(entityName, id, "totalAmountDistributedUntilUpdatedAt", totalAmountDistributedUntilUpdatedAt.toString()); assert.fieldEquals(entityName, id, "totalSupply", totalSupply.toString()); @@ -209,14 +236,24 @@ export function assertTokenStatisticProperties( event, triggeredByEventName, totalNumberOfActiveStreams, + totalCFANumberOfActiveStreams, + totalGDANumberOfActiveStreams, totalNumberOfClosedStreams, + totalCFANumberOfClosedStreams, + totalGDANumberOfClosedStreams, totalNumberOfIndexes, totalNumberOfActiveIndexes, totalSubscriptionsWithUnits, totalApprovedSubscriptions, totalDeposit, + totalCFADeposit, + totalGDADeposit, totalOutflowRate, + totalCFAOutflowRate, + totalGDAOutflowRate, totalAmountStreamedUntilUpdatedAt, + totalCFAAmountStreamedUntilUpdatedAt, + totalGDAAmountStreamedUntilUpdatedAt, totalAmountTransferredUntilUpdatedAt, totalAmountDistributedUntilUpdatedAt, totalSupply, @@ -250,15 +287,25 @@ export function assertTokenStatisticProperties( * Asserts that the properties on a TokenStatisticLog entity are correct. * @param event ethereum event object * @param triggeredByEventName name of the event which triggered the creation of this log - * @param totalNumberOfActiveStreams expected count of active streams for the token - * @param totalNumberOfClosedStreams expected count of closed streams for the token + * @param totalNumberOfActiveStreams expected count of active streams for the token for all flow agreements + * @param totalCFANumberOfActiveStreams expected count of active streams for the token for the CFA + * @param totalGDANumberOfActiveStreams expected count of active streams for the token for the GDA + * @param totalNumberOfClosedStreams expected count of closed streams for the token for all flow agreements + * @param totalNumberOfCFAClosedStreams expected count of closed streams for the token for the CFA + * @param totalNumberOfGDAClosedStreams expected count of closed streams for the token for the GDA * @param totalNumberOfIndexes expected count of indexes for the token * @param totalNumberOfActiveIndexes expected count of active indexes for the token * @param totalSubscriptionsWithUnits expected count of subscriptions with allocated units for the token * @param totalApprovedSubscriptions expected totalNumber of approved subscriptions for the token - * @param totalDeposit expected total deposit amount - * @param totalOutflowRate expected total outflow rate - * @param totalAmountStreamed expected total amount streamed until timestamp + * @param totalDeposit expected total deposit amount for all flow agreements + * @param totalCFADeposit expected total deposit amount for the CFA + * @param totalGDADeposit expected total deposit amount for the GDA + * @param totalOutflowRate expected total outflow rate for all flow agreements + * @param totalCFAOutflowRate expected total outflow rate for the CFA + * @param totalGDAOutflowRate expected total outflow rate for the GDA + * @param totalAmountStreamed expected total amount streamed until timestamp for all flow agreements + * @param totalCFAAmountStreamed expected total amount streamed until timestamp for the CFA + * @param totalGDAAmountStreamed expected total amount streamed until timestamp for the GDA * @param totalAmountTransferred expected total amount transferred until timestamp * @param totalAmountDistributed expected total amount distributed (with IDA) until timestamp * @param totalSupply expected total supply @@ -270,14 +317,24 @@ export function assertTokenStatisticLogProperties( event: ethereum.Event, triggeredByEventName: string, totalNumberOfActiveStreams: i32, + totalCFANumberOfActiveStreams: i32, + totalGDANumberOfActiveStreams: i32, totalNumberOfClosedStreams: i32, + totalCFANumberOfClosedStreams: i32, + totalGDANumberOfClosedStreams: i32, totalNumberOfIndexes: i32, totalNumberOfActiveIndexes: i32, totalSubscriptionsWithUnits: i32, totalApprovedSubscriptions: i32, totalDeposit: BigInt, + totalCFADeposit: BigInt, + totalGDADeposit: BigInt, totalOutflowRate: BigInt, + totalCFAOutflowRate: BigInt, + totalGDAOutflowRate: BigInt, totalAmountStreamed: BigInt, + totalCFAAmountStreamed: BigInt, + totalGDAAmountStreamed: BigInt, totalAmountTransferred: BigInt, totalAmountDistributed: BigInt, totalSupply: BigInt, @@ -298,19 +355,80 @@ export function assertTokenStatisticLogProperties( assert.fieldEquals(entityName, id, "order", order.toString()); assert.fieldEquals(entityName, id, "triggeredByEventName", triggeredByEventName); assert.fieldEquals(entityName, id, "totalNumberOfActiveStreams", totalNumberOfActiveStreams.toString()); + assert.fieldEquals(entityName, id, "totalCFANumberOfActiveStreams", totalCFANumberOfActiveStreams.toString()); + assert.fieldEquals(entityName, id, "totalGDANumberOfActiveStreams", totalGDANumberOfActiveStreams.toString()); + assert.fieldEquals(entityName, id, "totalNumberOfClosedStreams", totalNumberOfClosedStreams.toString()); + assert.fieldEquals(entityName, id, "totalCFANumberOfClosedStreams", totalCFANumberOfClosedStreams.toString()); + assert.fieldEquals(entityName, id, "totalGDANumberOfClosedStreams", totalGDANumberOfClosedStreams.toString()); assert.fieldEquals(entityName, id, "totalNumberOfClosedStreams", totalNumberOfClosedStreams.toString()); assert.fieldEquals(entityName, id, "totalNumberOfIndexes", totalNumberOfIndexes.toString()); assert.fieldEquals(entityName, id, "totalNumberOfActiveIndexes", totalNumberOfActiveIndexes.toString()); assert.fieldEquals(entityName, id, "totalSubscriptionsWithUnits", totalSubscriptionsWithUnits.toString()); assert.fieldEquals(entityName, id, "totalApprovedSubscriptions", totalApprovedSubscriptions.toString()); assert.fieldEquals(entityName, id, "totalDeposit", totalDeposit.toString()); + assert.fieldEquals(entityName, id, "totalCFADeposit", totalCFADeposit.toString()); + assert.fieldEquals(entityName, id, "totalGDADeposit", totalGDADeposit.toString()); assert.fieldEquals(entityName, id, "totalOutflowRate", totalOutflowRate.toString()); + assert.fieldEquals(entityName, id, "totalCFAOutflowRate", totalCFAOutflowRate.toString()); + assert.fieldEquals(entityName, id, "totalGDAOutflowRate", totalGDAOutflowRate.toString()); assert.fieldEquals(entityName, id, "totalAmountStreamed", totalAmountStreamed.toString()); + assert.fieldEquals(entityName, id, "totalCFAAmountStreamed", totalCFAAmountStreamed.toString()); + assert.fieldEquals(entityName, id, "totalGDAAmountStreamed", totalGDAAmountStreamed.toString()); assert.fieldEquals(entityName, id, "totalAmountTransferred", totalAmountTransferred.toString()); assert.fieldEquals(entityName, id, "totalAmountDistributed", totalAmountDistributed.toString()); assert.fieldEquals(entityName, id, "totalSupply", totalSupply.toString()); assert.fieldEquals(entityName, id, "token", tokenAddress); - assert.fieldEquals(entityName, id, "totalNumberOfAccounts", totalNumberOfAccounts.toString()); assert.fieldEquals(entityName, id, "tokenStatistic", tokenAddress); + assert.fieldEquals(entityName, id, "totalNumberOfAccounts", totalNumberOfAccounts.toString()); assert.fieldEquals(entityName, id, "totalNumberOfHolders", totalNumberOfHolders.toString()); +} + +/** + * Asserts that the properties on an "empty" initialized TokenStatistic entity are correct. + * @param id the token address + * @param event if event is passed, we validate TokenStatisticLog + * @param triggeredByEventName if triggeredByEventName is passed, we validate TokenStatisticLog + * @param updatedAtTimestamp timestamp retrieved from the event + * @param updatedAtBlockNumber block number retrieved from the event + * @param totalSupply expected total supply + */ +export function assertEmptyTokenStatisticProperties( + event: ethereum.Event | null, + triggeredByEventName: string | null, + id: string, + updatedAtTimestamp: BigInt, + updatedAtBlockNumber: BigInt, + totalSupply: BigInt +): void { + assertTokenStatisticProperties( + event, + triggeredByEventName, + id, + updatedAtTimestamp, + updatedAtBlockNumber, + 0, // totalNumberOfActiveStreams + 0, // totalCFANumberOfActiveStreams + 0, // totalGDANumberOfActiveStreams + 0, // totalNumberOfClosedStreams + 0, // totalCFANumberOfClosedStreams + 0, // totalGDANumberOfClosedStreams + 0, // totalNumberOfIndexes + 0, // totalNumberOfActiveIndexes + 0, // totalSubscriptionsWithUnits + 0, // totalApprovedSubscriptions + BIG_INT_ZERO, // totalDeposit + BIG_INT_ZERO, // totalCFADeposit + BIG_INT_ZERO, // totalGDADeposit + BIG_INT_ZERO, // totalOutflowRate + BIG_INT_ZERO, // totalCFAOutflowRate + BIG_INT_ZERO, // totalGDAOutflowRate + BIG_INT_ZERO, // totalAmountStreamedUntilUpdatedAt + BIG_INT_ZERO, // totalCFAAmountStreamedUntilUpdatedAt + BIG_INT_ZERO, // totalGDAAmountStreamedUntilUpdatedAt + BIG_INT_ZERO, // totalAmountTransferredUntilUpdatedAt + BIG_INT_ZERO, // totalAmountDistributedUntilUpdatedAt + totalSupply, // totalSupply + 0, // totalNumberOfAccounts + 0 // totalNumberOfHolders + ) } \ No newline at end of file diff --git a/packages/subgraph/tests/cfav1/hol/cfav1.hol.test.ts b/packages/subgraph/tests/cfav1/hol/cfav1.hol.test.ts index da836836d1..5beb4a9368 100644 --- a/packages/subgraph/tests/cfav1/hol/cfav1.hol.test.ts +++ b/packages/subgraph/tests/cfav1/hol/cfav1.hol.test.ts @@ -1,23 +1,21 @@ import { Address, BigInt } from "@graphprotocol/graph-ts"; +import { assert, beforeEach, clearStore, describe, test } from "matchstick-as/assembly/index"; +import { handleFlowOperatorUpdated } from "../../../src/mappings/cfav1"; import { - assert, - beforeEach, - clearStore, - describe, - test, -} from "matchstick-as/assembly/index"; -import { - handleFlowOperatorUpdated, -} from "../../../src/mappings/cfav1"; -import { BIG_INT_ZERO, getAccountTokenSnapshotID, getFlowOperatorID, getStreamID, ZERO_ADDRESS } from "../../../src/utils"; -import { - assertHigherOrderBaseProperties, -} from "../../assertionHelpers"; + BIG_INT_ZERO, + getAccountTokenSnapshotID, + getFlowOperatorID, + getStreamID, + ZERO_ADDRESS, +} from "../../../src/utils"; +import { assertHigherOrderBaseProperties } from "../../assertionHelpers"; import { alice, bob, maticXAddress, maticXName, maticXSymbol } from "../../constants"; import { - createFlowOperatorUpdatedEvent, getDeposit, modifyFlowAndAssertFlowUpdatedEventProperties, + createFlowOperatorUpdatedEvent, + getDeposit, + modifyFlowAndAssertFlowUpdatedEventProperties, } from "../cfav1.helper"; -import {mockedApprove} from "../../mockedFunctions"; +import { mockedApprove } from "../../mockedFunctions"; const initialFlowRate = BigInt.fromI32(100); @@ -57,11 +55,7 @@ describe("ConstantFlowAgreementV1 Higher Order Level Entity Unit Tests", () => { BIG_INT_ZERO ); - assert.fieldEquals("Stream", id, "id", id); - assert.fieldEquals("Stream", id, "createdAtTimestamp", flowUpdatedEvent.block.timestamp.toString()); - assert.fieldEquals("Stream", id, "createdAtBlockNumber", flowUpdatedEvent.block.number.toString()); - assert.fieldEquals("Stream", id, "updatedAtTimestamp", flowUpdatedEvent.block.timestamp.toString()); - assert.fieldEquals("Stream", id, "updatedAtBlockNumber", flowUpdatedEvent.block.number.toString()); + assertHigherOrderBaseProperties("Stream", id, flowUpdatedEvent); assert.fieldEquals("Stream", id, "currentFlowRate", flowUpdatedEvent.params.flowRate.toString()); assert.fieldEquals("Stream", id, "deposit", deposit.toString()); assert.fieldEquals("Stream", id, "streamedUntilUpdatedAt", streamedUntilUpdatedAt.toString()); @@ -98,14 +92,7 @@ describe("ConstantFlowAgreementV1 Higher Order Level Entity Unit Tests", () => { Address.fromString(sender) ); const atsId = getAccountTokenSnapshotID(Address.fromString(sender), Address.fromString(superToken)); - assertHigherOrderBaseProperties( - "FlowOperator", - id, - flowOperatorUpdatedEvent.block.timestamp, - flowOperatorUpdatedEvent.block.number, - flowOperatorUpdatedEvent.block.timestamp, - flowOperatorUpdatedEvent.block.number - ); + assertHigherOrderBaseProperties("FlowOperator", id, flowOperatorUpdatedEvent); assert.fieldEquals("FlowOperator", id, "permissions", permissions.toString()); assert.fieldEquals("FlowOperator", id, "flowRateAllowanceGranted", flowRateAllowance.toString()); assert.fieldEquals("FlowOperator", id, "flowRateAllowanceRemaining", flowRateAllowance.toString()); @@ -117,7 +104,6 @@ describe("ConstantFlowAgreementV1 Higher Order Level Entity Unit Tests", () => { }); }); - /** * Calculates the streamedUntilUpdatedAt. * @param streamedSoFar @@ -132,7 +118,5 @@ function _getStreamedUntilUpdatedAt( lastUpdatedAtTime: BigInt, previousOutflowRate: BigInt ): BigInt { - return streamedSoFar.plus( - previousOutflowRate.times(currentTime.minus(lastUpdatedAtTime)) - ); + return streamedSoFar.plus(previousOutflowRate.times(currentTime.minus(lastUpdatedAtTime))); } diff --git a/packages/subgraph/tests/constants.ts b/packages/subgraph/tests/constants.ts index c4582b3b10..1c15feacfc 100644 --- a/packages/subgraph/tests/constants.ts +++ b/packages/subgraph/tests/constants.ts @@ -18,21 +18,23 @@ export const bob = "0x70997970c51812dc3a010c7d01b50e0d17dc79c8"; export const charlie = "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc"; export const delta = "0x90f79bf6eb2c4f870365e785982e1f101e93b906"; export const echo = "0x15d34aaf54267db7d7c367839aaf71a00a2c6a65"; +export const superfluidPool = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; // contract addresses (polygon) export const hostAddress = "0x3e14dc1b13c488a8d5d310918780c983bd5982e7"; export const cfaV1Address = "0x6eee6060f715257b970700bc2656de21dedf074c"; export const idaV1Address = "0xb0aabba4b2783a72c52956cdef62d438eca2d7a1"; export const superTokenFactoryAddress = "0x2c90719f25b10fc5646c82da3240c76fa5bccf34"; -export const superTokenLogicAddress = "0xd15c6953c0a7fcc324e835f397496d53751441e2"; +export const superTokenLogicAddress = "0x1349b5f1006ef0366a7b6ae41fa9155c6cd91e4b"; export const resolverAddress = "0xe0cc76334405ee8b39213e620587d815967af39c"; +// this is not the actual TOGA export const togaAddress = "0x6aeaee5fd4d05a741723d752d30ee4d72690a8f7"; export const maticXAddress = "0x3ad736904e9e65189c3000c7dd2c8ac8bb7cd4e3"; export const maticXSymbol = "MATICx"; export const maticXName = "Super MATIC"; -export const daiXAddress = "0x1305f6b6df9dc47159d12eb7ac2804d4a33173c2"; +export const daiXAddress = "0x5d8b4c2554aeb7e86f387b4d6c00ac33499ed01f"; export const daiXSymbol = "DAIx"; export const daiXName = "Super DAI (PoS)"; -export const daiAddress = "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063"; +export const daiAddress = "0x15f0ca26781c3852f8166ed2ebce5d18265cceb7"; export const daiSymbol = "DAI"; export const daiName = "(PoS) Dai Stablecoin (DAI)"; diff --git a/packages/subgraph/tests/gdav1/event/gdav1.event.test.ts b/packages/subgraph/tests/gdav1/event/gdav1.event.test.ts new file mode 100644 index 0000000000..ee21c7b029 --- /dev/null +++ b/packages/subgraph/tests/gdav1/event/gdav1.event.test.ts @@ -0,0 +1,260 @@ +import { Address, BigInt } from "@graphprotocol/graph-ts"; +import { assert, beforeEach, clearStore, describe, test } from "matchstick-as/assembly/index"; +import { + handleBufferAdjusted, + handleFlowDistributionUpdated, + handleInstantDistributionUpdated, +} from "../../../src/mappings/gdav1"; +import { handleDistributionClaimed } from "../../../src/mappings/superfluidPool"; +import { BIG_INT_ZERO, getPoolDistributorID, getPoolMemberID } from "../../../src/utils"; +import { assertEventBaseProperties } from "../../assertionHelpers"; +import { FAKE_INITIAL_BALANCE, FALSE, TRUE, alice, bob, maticXAddress, superfluidPool } from "../../constants"; +import { + createBufferAdjustedEvent, + createDistributionClaimedEvent, + createFlowDistributionUpdatedEvent, + createInstantDistributionUpdatedEvent, + createPoolAndReturnPoolCreatedEvent +} from "../gdav1.helper"; +import { mockedGetAppManifest, mockedRealtimeBalanceOf } from "../../mockedFunctions"; +import { updatePoolConnectionAndReturnPoolConnectionUpdatedEvent } from "../gdav1.helper"; +import { updateMemberUnitsAndReturnMemberUnitsUpdatedEvent } from "../gdav1.helper"; +import { stringToBytes } from "../../converters"; + +const initialFlowRate = BigInt.fromI32(100); +const superToken = maticXAddress; + +describe("GeneralDistributionAgreementV1 Event Entity Unit Tests", () => { + beforeEach(() => { + clearStore(); + }); + + test("handlePoolCreated() - Should create a new PoolCreatedEvent entity", () => { + const admin = bob; + const poolCreatedEvent = createPoolAndReturnPoolCreatedEvent(admin, superToken, superfluidPool); + + const id = assertEventBaseProperties(poolCreatedEvent, "PoolCreated"); + assert.fieldEquals("PoolCreatedEvent", id, "token", superToken); + assert.fieldEquals("PoolCreatedEvent", id, "caller", poolCreatedEvent.transaction.from.toHexString()); + assert.fieldEquals("PoolCreatedEvent", id, "admin", admin); + assert.fieldEquals("PoolCreatedEvent", id, "pool", superfluidPool); + }); + + test("handlePoolConnectionUpdated() - Should create a new handlePoolConnectionUpdatedEvent entity (connected)", () => { + const account = bob; + const connected = true; + const userData = stringToBytes(""); + + const poolConnectionUpdatedEvent = updatePoolConnectionAndReturnPoolConnectionUpdatedEvent( + superToken, + account, + superfluidPool, + connected, + initialFlowRate, + userData + ); + + const poolMemberId = getPoolMemberID(Address.fromString(superfluidPool), Address.fromString(account)); + + const id = assertEventBaseProperties(poolConnectionUpdatedEvent, "PoolConnectionUpdated"); + assert.fieldEquals("PoolConnectionUpdatedEvent", id, "token", superToken); + assert.fieldEquals("PoolConnectionUpdatedEvent", id, "connected", TRUE); + assert.fieldEquals("PoolConnectionUpdatedEvent", id, "pool", superfluidPool); + assert.fieldEquals("PoolConnectionUpdatedEvent", id, "poolMember", poolMemberId); + }); + + test("handlePoolConnectionUpdated() - Should create a new handlePoolConnectionUpdatedEvent entity (disconnected)", () => { + const account = bob; + const connected = false; + const userData = stringToBytes(""); + + const poolConnectionUpdatedEvent = updatePoolConnectionAndReturnPoolConnectionUpdatedEvent( + superToken, + account, + superfluidPool, + connected, + initialFlowRate, + userData + ); + + const poolMemberId = getPoolMemberID(Address.fromString(superfluidPool), Address.fromString(account)); + + const id = assertEventBaseProperties(poolConnectionUpdatedEvent, "PoolConnectionUpdated"); + assert.fieldEquals("PoolConnectionUpdatedEvent", id, "token", superToken); + assert.fieldEquals("PoolConnectionUpdatedEvent", id, "connected", FALSE); + assert.fieldEquals("PoolConnectionUpdatedEvent", id, "pool", superfluidPool); + assert.fieldEquals("PoolConnectionUpdatedEvent", id, "poolMember", poolMemberId); + }); + + test("handleBufferAdjusted() - Should create a new handleBufferAdjustedEvent entity", () => { + const bufferDelta = BigInt.fromI32(69); + const newBufferAmount = BigInt.fromI32(420); + const totalBufferAmount = BigInt.fromI32(42069); + const poolDistributor = alice; + + const bufferAdjustedEvent = createBufferAdjustedEvent( + maticXAddress, + superfluidPool, + poolDistributor, + bufferDelta, + newBufferAmount, + totalBufferAmount + ); + + const poolDistributorId = getPoolDistributorID( + Address.fromString(superfluidPool), + Address.fromString(poolDistributor) + ); + + handleBufferAdjusted(bufferAdjustedEvent); + + const id = assertEventBaseProperties(bufferAdjustedEvent, "BufferAdjusted"); + assert.fieldEquals("BufferAdjustedEvent", id, "token", maticXAddress); + assert.fieldEquals("BufferAdjustedEvent", id, "pool", superfluidPool); + assert.fieldEquals("BufferAdjustedEvent", id, "poolDistributor", poolDistributorId.toString()); + assert.fieldEquals("BufferAdjustedEvent", id, "bufferDelta", bufferDelta.toString()); + assert.fieldEquals("BufferAdjustedEvent", id, "newBufferAmount", newBufferAmount.toString()); + assert.fieldEquals("BufferAdjustedEvent", id, "totalBufferAmount", totalBufferAmount.toString()); + }); + + test("handleInstantDistributionUpdated() - Should create a new handleInstantDistributionUpdatedEvent entity", () => { + const operator = alice; + const requestedAmount = BigInt.fromI32(69); + const actualAmount = BigInt.fromI32(70); + const poolDistributor = bob; + const userData = stringToBytes(""); + + const instantDistributionUpdatedEvent = createInstantDistributionUpdatedEvent( + superToken, + superfluidPool, + poolDistributor, + operator, + requestedAmount, + actualAmount, + userData + ); + + const poolDistributorId = getPoolDistributorID( + Address.fromString(superfluidPool), + Address.fromString(poolDistributor) + ); + + handleInstantDistributionUpdated(instantDistributionUpdatedEvent); + + const id = assertEventBaseProperties(instantDistributionUpdatedEvent, "InstantDistributionUpdated"); + assert.fieldEquals("InstantDistributionUpdatedEvent", id, "token", superToken); + assert.fieldEquals("InstantDistributionUpdatedEvent", id, "pool", superfluidPool); + assert.fieldEquals("InstantDistributionUpdatedEvent", id, "poolDistributor", poolDistributorId.toString()); + assert.fieldEquals("InstantDistributionUpdatedEvent", id, "operator", operator); + assert.fieldEquals("InstantDistributionUpdatedEvent", id, "requestedAmount", requestedAmount.toString()); + assert.fieldEquals("InstantDistributionUpdatedEvent", id, "actualAmount", actualAmount.toString()); + }); + + test("handleFlowDistributionUpdated() - Should create a new handleFlowDistributionUpdatedEvent entity", () => { + const operator = alice; + const oldFlowRate = BigInt.fromI32(69); + const newDistributorToPoolFlowRate = BigInt.fromI32(420); + const newTotalDistributionFlowRate = BigInt.fromI32(42069); + const adjustmentFlowRecipient = alice; + const adjustmentFlowRate = BigInt.fromI32(5); + const poolDistributor = bob; + const userData = stringToBytes(""); + + const flowDistributionUpdatedEvent = createFlowDistributionUpdatedEvent( + superToken, + superfluidPool, + poolDistributor, + operator, + oldFlowRate, + newDistributorToPoolFlowRate, + newTotalDistributionFlowRate, + adjustmentFlowRecipient, + adjustmentFlowRate, + userData + ); + + const poolDistributorId = getPoolDistributorID( + Address.fromString(superfluidPool), + Address.fromString(poolDistributor) + ); + + handleFlowDistributionUpdated(flowDistributionUpdatedEvent); + + const id = assertEventBaseProperties(flowDistributionUpdatedEvent, "FlowDistributionUpdated"); + assert.fieldEquals("FlowDistributionUpdatedEvent", id, "token", superToken); + assert.fieldEquals("FlowDistributionUpdatedEvent", id, "pool", superfluidPool); + assert.fieldEquals("FlowDistributionUpdatedEvent", id, "poolDistributor", poolDistributorId.toString()); + assert.fieldEquals("FlowDistributionUpdatedEvent", id, "operator", operator); + assert.fieldEquals("FlowDistributionUpdatedEvent", id, "oldFlowRate", oldFlowRate.toString()); + assert.fieldEquals( + "FlowDistributionUpdatedEvent", + id, + "newDistributorToPoolFlowRate", + newDistributorToPoolFlowRate.toString() + ); + assert.fieldEquals( + "FlowDistributionUpdatedEvent", + id, + "newTotalDistributionFlowRate", + newTotalDistributionFlowRate.toString() + ); + assert.fieldEquals("FlowDistributionUpdatedEvent", id, "adjustmentFlowRecipient", adjustmentFlowRecipient); + assert.fieldEquals("FlowDistributionUpdatedEvent", id, "adjustmentFlowRate", adjustmentFlowRate.toString()); + }); + + test("handleDistributionClaimed() - Should create a new DistributionClaimedEvent entity", () => { + const poolMember = alice; + const claimedAmount = BigInt.fromI32(69); + const totalClaimed = BigInt.fromI32(420); + + const distributionClaimedEvent = createDistributionClaimedEvent( + superToken, + poolMember, + claimedAmount, + totalClaimed + ); + + // getOrInitAccountTokenSnapshot(event) => getOrInitAccount(account) => host.try_getAppManifest(account) + mockedGetAppManifest(poolMember, false, false, BIG_INT_ZERO); + + // updateATSStreamedAndBalanceUntilUpdatedAt => updateATSBalanceAndUpdatedAt => try_realtimeBalanceOf(poolMember) + mockedRealtimeBalanceOf( + superToken, + poolMember, + distributionClaimedEvent.block.timestamp, + FAKE_INITIAL_BALANCE.plus(initialFlowRate), + initialFlowRate, + BIG_INT_ZERO + ); + + const poolMemberId = getPoolMemberID(distributionClaimedEvent.address, Address.fromString(poolMember)); + + handleDistributionClaimed(distributionClaimedEvent); + + const id = assertEventBaseProperties(distributionClaimedEvent, "DistributionClaimed"); + assert.fieldEquals("DistributionClaimedEvent", id, "token", superToken); + assert.fieldEquals("DistributionClaimedEvent", id, "claimedAmount", claimedAmount.toString()); + assert.fieldEquals("DistributionClaimedEvent", id, "totalClaimed", totalClaimed.toString()); + assert.fieldEquals("DistributionClaimedEvent", id, "poolMember", poolMemberId.toString()); + }); + + test("handleMemberUnitsUpdated() - Should create a new MemberUnitsUpdatedEvent entity", () => { + const oldUnits = BigInt.fromI32(0); + const newUnits = BigInt.fromI32(69); + const poolMember = bob; + + const memberUnitsUpdatedEvent = updateMemberUnitsAndReturnMemberUnitsUpdatedEvent( + superToken, + poolMember, + oldUnits, + newUnits + ); + + const poolMemberId = getPoolMemberID(memberUnitsUpdatedEvent.address, Address.fromString(poolMember)); + + const id = assertEventBaseProperties(memberUnitsUpdatedEvent, "MemberUnitsUpdated"); + assert.fieldEquals("MemberUnitsUpdatedEvent", id, "token", superToken); + assert.fieldEquals("MemberUnitsUpdatedEvent", id, "poolMember", poolMemberId.toString()); + assert.fieldEquals("MemberUnitsUpdatedEvent", id, "units", newUnits.toString()); + }); +}); diff --git a/packages/subgraph/tests/gdav1/gdav1.helper.ts b/packages/subgraph/tests/gdav1/gdav1.helper.ts new file mode 100644 index 0000000000..d170b5b2fa --- /dev/null +++ b/packages/subgraph/tests/gdav1/gdav1.helper.ts @@ -0,0 +1,227 @@ +import { newMockEvent } from "matchstick-as"; +import { + BufferAdjusted, + FlowDistributionUpdated, + InstantDistributionUpdated, + PoolConnectionUpdated, + PoolCreated, +} from "../../generated/GeneralDistributionAgreementV1/IGeneralDistributionAgreementV1"; +import { + DistributionClaimed, + MemberUnitsUpdated, +} from "../../generated/GeneralDistributionAgreementV1/ISuperfluidPool"; +import { getAddressEventParam, getBigIntEventParam, getBooleanEventParam, getBytesEventParam } from "../converters"; +import { BigInt, Bytes } from "@graphprotocol/graph-ts"; +import { handlePoolConnectionUpdated, handlePoolCreated } from "../../src/mappings/gdav1"; +import { BIG_INT_ZERO } from "../../src/utils"; +import { FAKE_INITIAL_BALANCE } from "../constants"; +import { mockedGetAppManifest, mockedRealtimeBalanceOf } from "../mockedFunctions"; +import { handleMemberUnitsUpdated } from "../../src/mappings/superfluidPool"; + +export function createPoolAndReturnPoolCreatedEvent( + admin: string, + superToken: string, + superfluidPool: string, + initialFlowRate: BigInt = BIG_INT_ZERO +): PoolCreated { + const poolCreatedEvent = createPoolCreatedEvent(superToken, admin, superfluidPool); + + // getOrInitAccountTokenSnapshot(event) => getOrInitAccount(admin) => host.try_getAppManifest(admin) + mockedGetAppManifest(admin, false, false, BIG_INT_ZERO); + + // updateATSStreamedAndBalanceUntilUpdatedAt => updateATSBalanceAndUpdatedAt => try_realtimeBalanceOf(admin) + mockedRealtimeBalanceOf( + superToken, + admin, + poolCreatedEvent.block.timestamp, + FAKE_INITIAL_BALANCE.plus(initialFlowRate), + initialFlowRate, + BIG_INT_ZERO + ); + + handlePoolCreated(poolCreatedEvent); + return poolCreatedEvent; +} + +export function updatePoolConnectionAndReturnPoolConnectionUpdatedEvent( + superToken: string, + account: string, + superfluidPool: string, + connected: boolean, + initialFlowRate: BigInt, + userData: Bytes +): PoolConnectionUpdated { + const poolConnectionUpdatedEvent = createPoolConnectionUpdatedEvent( + superToken, + superfluidPool, + account, + connected, + userData + ); + + // getOrInitAccountTokenSnapshot(event) => getOrInitAccount(account) => host.try_getAppManifest(account) + mockedGetAppManifest(account, false, false, BIG_INT_ZERO); + + // updateATSStreamedAndBalanceUntilUpdatedAt => updateATSBalanceAndUpdatedAt => try_realtimeBalanceOf(account) + mockedRealtimeBalanceOf( + superToken, + account, + poolConnectionUpdatedEvent.block.timestamp, + FAKE_INITIAL_BALANCE.plus(initialFlowRate), + initialFlowRate, + BIG_INT_ZERO + ); + + handlePoolConnectionUpdated(poolConnectionUpdatedEvent); + return poolConnectionUpdatedEvent; +} + +export function updateMemberUnitsAndReturnMemberUnitsUpdatedEvent( + superToken: string, + poolMember: string, + oldUnits: BigInt, + newUnits: BigInt +): MemberUnitsUpdated { + const memberUnitsUpdatedEvent = createMemberUnitsUpdatedEvent(superToken, poolMember, oldUnits, newUnits); + + handleMemberUnitsUpdated(memberUnitsUpdatedEvent); + + return memberUnitsUpdatedEvent; +} + +// Mock Event Creators +export function createPoolCreatedEvent(token: string, admin: string, pool: string): PoolCreated { + const newPoolCreatedEvent = changetype(newMockEvent()); + newPoolCreatedEvent.parameters = new Array(); + newPoolCreatedEvent.parameters.push(getAddressEventParam("token", token)); + newPoolCreatedEvent.parameters.push(getAddressEventParam("admin", admin)); + newPoolCreatedEvent.parameters.push(getAddressEventParam("pool", pool)); + + return newPoolCreatedEvent; +} + +export function createPoolConnectionUpdatedEvent( + token: string, + pool: string, + poolMember: string, + connected: boolean, + userData: Bytes +): PoolConnectionUpdated { + const newPoolConnectionUpdatedEvent = changetype(newMockEvent()); + newPoolConnectionUpdatedEvent.parameters = new Array(); + newPoolConnectionUpdatedEvent.parameters.push(getAddressEventParam("token", token)); + newPoolConnectionUpdatedEvent.parameters.push(getAddressEventParam("pool", pool)); + newPoolConnectionUpdatedEvent.parameters.push(getAddressEventParam("poolMember", poolMember)); + newPoolConnectionUpdatedEvent.parameters.push(getBooleanEventParam("connected", connected)); + newPoolConnectionUpdatedEvent.parameters.push(getBytesEventParam("userData", userData)); + + return newPoolConnectionUpdatedEvent; +} + +export function createBufferAdjustedEvent( + token: string, + pool: string, + poolDistributor: string, + bufferDelta: BigInt, + newBufferAmount: BigInt, + totalBufferAmount: BigInt +): BufferAdjusted { + const newBufferAdjustedEvent = changetype(newMockEvent()); + newBufferAdjustedEvent.parameters = new Array(); + newBufferAdjustedEvent.parameters.push(getAddressEventParam("token", token)); + newBufferAdjustedEvent.parameters.push(getAddressEventParam("pool", pool)); + newBufferAdjustedEvent.parameters.push(getAddressEventParam("poolDistributor", poolDistributor)); + newBufferAdjustedEvent.parameters.push(getBigIntEventParam("bufferDelta", bufferDelta)); + newBufferAdjustedEvent.parameters.push(getBigIntEventParam("newBufferAmount", newBufferAmount)); + newBufferAdjustedEvent.parameters.push(getBigIntEventParam("totalBufferAmount", totalBufferAmount)); + + return newBufferAdjustedEvent; +} + +export function createInstantDistributionUpdatedEvent( + token: string, + pool: string, + poolDistributor: string, + operator: string, + requestedAmount: BigInt, + actualAmount: BigInt, + userData: Bytes +): InstantDistributionUpdated { + const newInstantDistributionUpdatedEvent = changetype(newMockEvent()); + newInstantDistributionUpdatedEvent.parameters = new Array(); + newInstantDistributionUpdatedEvent.parameters.push(getAddressEventParam("token", token)); + newInstantDistributionUpdatedEvent.parameters.push(getAddressEventParam("pool", pool)); + newInstantDistributionUpdatedEvent.parameters.push(getAddressEventParam("poolDistributor", poolDistributor)); + newInstantDistributionUpdatedEvent.parameters.push(getAddressEventParam("operator", operator)); + newInstantDistributionUpdatedEvent.parameters.push(getBigIntEventParam("requestedAmount", requestedAmount)); + newInstantDistributionUpdatedEvent.parameters.push(getBigIntEventParam("actualAmount", actualAmount)); + newInstantDistributionUpdatedEvent.parameters.push(getBytesEventParam("userData", userData)); + + return newInstantDistributionUpdatedEvent; +} + +export function createFlowDistributionUpdatedEvent( + token: string, + pool: string, + poolDistributor: string, + operator: string, + oldFlowRate: BigInt, + newDistributorToPoolFlowRate: BigInt, + newTotalDistributionFlowRate: BigInt, + adjustmentFlowRecipient: string, + adjustmentFlowRate: BigInt, + userData: Bytes +): FlowDistributionUpdated { + const newFlowDistributionUpdatedEvent = changetype(newMockEvent()); + newFlowDistributionUpdatedEvent.parameters = new Array(); + newFlowDistributionUpdatedEvent.parameters.push(getAddressEventParam("token", token)); + newFlowDistributionUpdatedEvent.parameters.push(getAddressEventParam("pool", pool)); + newFlowDistributionUpdatedEvent.parameters.push(getAddressEventParam("poolDistributor", poolDistributor)); + newFlowDistributionUpdatedEvent.parameters.push(getAddressEventParam("operator", operator)); + newFlowDistributionUpdatedEvent.parameters.push(getBigIntEventParam("oldFlowRate", oldFlowRate)); + newFlowDistributionUpdatedEvent.parameters.push( + getBigIntEventParam("newDistributorToPoolFlowRate", newDistributorToPoolFlowRate) + ); + newFlowDistributionUpdatedEvent.parameters.push( + getBigIntEventParam("newTotalDistributionFlowRate", newTotalDistributionFlowRate) + ); + newFlowDistributionUpdatedEvent.parameters.push( + getAddressEventParam("adjustmentFlowRecipient", adjustmentFlowRecipient) + ); + newFlowDistributionUpdatedEvent.parameters.push(getBigIntEventParam("adjustmentFlowRate", adjustmentFlowRate)); + newFlowDistributionUpdatedEvent.parameters.push(getBytesEventParam("userData", userData)); + + return newFlowDistributionUpdatedEvent; +} + +export function createDistributionClaimedEvent( + token: string, + poolMember: string, + claimedAmount: BigInt, + totalClaimed: BigInt +): DistributionClaimed { + const newDistributionClaimedEvent = changetype(newMockEvent()); + newDistributionClaimedEvent.parameters = new Array(); + newDistributionClaimedEvent.parameters.push(getAddressEventParam("token", token)); + newDistributionClaimedEvent.parameters.push(getAddressEventParam("poolMember", poolMember)); + newDistributionClaimedEvent.parameters.push(getBigIntEventParam("claimedAmount", claimedAmount)); + newDistributionClaimedEvent.parameters.push(getBigIntEventParam("totalClaimed", totalClaimed)); + + return newDistributionClaimedEvent; +} + +export function createMemberUnitsUpdatedEvent( + token: string, + poolMember: string, + oldUnits: BigInt, + newUnits: BigInt +): MemberUnitsUpdated { + const newMemberUnitsUpdatedEvent = changetype(newMockEvent()); + newMemberUnitsUpdatedEvent.parameters = new Array(); + newMemberUnitsUpdatedEvent.parameters.push(getAddressEventParam("token", token)); + newMemberUnitsUpdatedEvent.parameters.push(getAddressEventParam("poolMember", poolMember)); + newMemberUnitsUpdatedEvent.parameters.push(getBigIntEventParam("oldUnits", oldUnits)); + newMemberUnitsUpdatedEvent.parameters.push(getBigIntEventParam("newUnits", newUnits)); + + return newMemberUnitsUpdatedEvent; +} diff --git a/packages/subgraph/tests/gdav1/hol/gdav1.hol.test.ts b/packages/subgraph/tests/gdav1/hol/gdav1.hol.test.ts new file mode 100644 index 0000000000..c0e2cfc3f7 --- /dev/null +++ b/packages/subgraph/tests/gdav1/hol/gdav1.hol.test.ts @@ -0,0 +1,693 @@ +import { Address, BigInt, ethereum } from "@graphprotocol/graph-ts"; +import { assert, beforeEach, clearStore, describe, test } from "matchstick-as/assembly/index"; +import { BIG_INT_ONE, BIG_INT_ZERO, getPoolDistributorID, getPoolMemberID, ZERO_ADDRESS } from "../../../src/utils"; +import { assertHigherOrderBaseProperties } from "../../assertionHelpers"; +import { FALSE, TRUE, alice, bob, maticXAddress, superfluidPool } from "../../constants"; +import { + createBufferAdjustedEvent, + createDistributionClaimedEvent, + createFlowDistributionUpdatedEvent, + createInstantDistributionUpdatedEvent, + createMemberUnitsUpdatedEvent, + createPoolAndReturnPoolCreatedEvent, + updatePoolConnectionAndReturnPoolConnectionUpdatedEvent, +} from "../gdav1.helper"; +import { + handleBufferAdjusted, + handleFlowDistributionUpdated, + handleInstantDistributionUpdated, +} from "../../../src/mappings/gdav1"; +import { updateMemberUnitsAndReturnMemberUnitsUpdatedEvent } from "../gdav1.helper"; +import { handleDistributionClaimed, handleMemberUnitsUpdated } from "../../../src/mappings/superfluidPool"; +import { getOrInitPoolMember } from "../../../src/mappingHelpers"; +import { stringToBytes } from "../../converters"; + +const initialFlowRate = BigInt.fromI32(100); +const superToken = maticXAddress; +const admin = alice; + +describe("GeneralDistributionAgreementV1 Higher Order Level Entity Unit Tests", () => { + beforeEach(() => { + clearStore(); + }); + + test("handlePoolCreated() - Should create a new Pool entity (create)", () => { + const poolCreatedEvent = createPoolAndReturnPoolCreatedEvent(admin, superToken, superfluidPool); + + const id = superfluidPool; + assertEmptyPoolData(id, poolCreatedEvent, superToken); + }); + + test("handlePoolConnectionUpdated() - Non-Member (0 units) connection updated: Pool entity is unchanged", () => { + createPoolAndReturnPoolCreatedEvent(admin, superToken, superfluidPool); + + const account = bob; + const connected = true; + const userData = stringToBytes(""); + + const poolConnectionUpdatedEvent = updatePoolConnectionAndReturnPoolConnectionUpdatedEvent( + superToken, + account, + superfluidPool, + connected, + initialFlowRate, + userData + ); + + const id = superfluidPool; + assertEmptyPoolData(id, poolConnectionUpdatedEvent, superToken); + }); + + test("handlePoolConnectionUpdated() - Member (>0 units) connection updated: Pool entity changes", () => { + const account = bob; + const connected = true; + const userData = stringToBytes(""); + + const memberUnitsUpdatedEvent = updateMemberUnitsAndReturnMemberUnitsUpdatedEvent( + superToken, + account, + BigInt.fromI32(0), + BigInt.fromI32(1) + ); + + const poolConnectionUpdatedEvent = updatePoolConnectionAndReturnPoolConnectionUpdatedEvent( + superToken, + account, + memberUnitsUpdatedEvent.address.toHexString(), + connected, + initialFlowRate, + userData + ); + + const id = memberUnitsUpdatedEvent.address.toHexString(); + + assertHigherOrderBaseProperties("Pool", id, poolConnectionUpdatedEvent); + assert.fieldEquals("Pool", id, "totalUnits", BIG_INT_ONE.toString()); + assert.fieldEquals("Pool", id, "totalConnectedUnits", BIG_INT_ONE.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountInstantlyDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountFlowedDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalMembers", BIG_INT_ONE.toString()); + assert.fieldEquals("Pool", id, "totalConnectedMembers", BIG_INT_ONE.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "adjustmentFlowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "flowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalBuffer", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "token", ZERO_ADDRESS.toHex()); + assert.fieldEquals("Pool", id, "admin", ZERO_ADDRESS.toHex()); + }); + + test("handlePoolConnectionUpdated() - Pool Entity: Disconnected member connection updated", () => { + const account = bob; + const connected = false; + const userData = stringToBytes(""); + + const memberUnitsUpdatedEvent = updateMemberUnitsAndReturnMemberUnitsUpdatedEvent( + superToken, + account, + BigInt.fromI32(0), + BigInt.fromI32(1) + ); + + const poolConnectionUpdatedEvent = updatePoolConnectionAndReturnPoolConnectionUpdatedEvent( + superToken, + account, + memberUnitsUpdatedEvent.address.toHexString(), + connected, + initialFlowRate, + userData + ); + + const id = memberUnitsUpdatedEvent.address.toHexString(); + + assertHigherOrderBaseProperties("Pool", id, poolConnectionUpdatedEvent); + assert.fieldEquals("Pool", id, "totalUnits", BIG_INT_ONE.toString()); + assert.fieldEquals("Pool", id, "totalConnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedUnits", BIG_INT_ONE.toString()); + assert.fieldEquals("Pool", id, "totalAmountInstantlyDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountFlowedDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalMembers", BIG_INT_ONE.toString()); + assert.fieldEquals("Pool", id, "totalConnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedMembers", BIG_INT_ONE.toString()); + assert.fieldEquals("Pool", id, "adjustmentFlowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "flowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalBuffer", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "token", ZERO_ADDRESS.toHex()); + assert.fieldEquals("Pool", id, "admin", ZERO_ADDRESS.toHex()); + }); + + test("handleBufferAdjusted() - Pool Entity: Total buffer value updated", () => { + const distributor = alice; + + const BUFFER = BigInt.fromI32(100); + + const bufferAdjustedEvent = createBufferAdjustedEvent( + superToken, // token + superfluidPool, // pool + distributor, // poolDistributor + BUFFER, // bufferDelta + BUFFER, // newBufferAmount + BUFFER // totalBufferAmount + ); + + handleBufferAdjusted(bufferAdjustedEvent); + + const id = bufferAdjustedEvent.params.pool.toHexString(); + + assertHigherOrderBaseProperties("Pool", id, bufferAdjustedEvent); + assert.fieldEquals("Pool", id, "totalUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalConnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountInstantlyDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountFlowedDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalConnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "adjustmentFlowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "flowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalBuffer", BUFFER.toString()); + assert.fieldEquals("Pool", id, "token", ZERO_ADDRESS.toHex()); + assert.fieldEquals("Pool", id, "admin", ZERO_ADDRESS.toHex()); + }); + + test("handleFlowDistributionUpdated() - Pool Entity: flow related fields updated:", () => { + const distributor = alice; + const operator = alice; + const emptyFlowRate = BigInt.fromI32(0); + const newFlowRate = BigInt.fromI32(100000000); + const userData = stringToBytes(""); + const flowDistributionUpdatedEvent = createFlowDistributionUpdatedEvent( + superToken, + superfluidPool, + distributor, + operator, + emptyFlowRate, // old flow rate + newFlowRate, // new distributor to pool flow rate + newFlowRate, // new total distribution flow rate + alice, // adjustment flow recipient + BigInt.fromI32(0), // adjustment flow rate + userData + ); + + handleFlowDistributionUpdated(flowDistributionUpdatedEvent); + + const id = flowDistributionUpdatedEvent.params.pool.toHexString(); + + assertHigherOrderBaseProperties("Pool", id, flowDistributionUpdatedEvent); + assert.fieldEquals("Pool", id, "totalUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalConnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountInstantlyDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountFlowedDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalConnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "adjustmentFlowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "flowRate", newFlowRate.toString()); + assert.fieldEquals("Pool", id, "totalBuffer", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "token", ZERO_ADDRESS.toHex()); + assert.fieldEquals("Pool", id, "admin", ZERO_ADDRESS.toHex()); + }); + + test("handleInstantDistributionUpdated() - Pool Entity: Total distributed amount updated:", () => { + const distributor = alice; + const operator = alice; + const requestedAmount = BigInt.fromI32(100000000); + const userData = stringToBytes(""); + const instantDistributionUpdatedEvent = createInstantDistributionUpdatedEvent( + superToken, + superfluidPool, + distributor, + operator, + requestedAmount, + requestedAmount, + userData + ); + + handleInstantDistributionUpdated(instantDistributionUpdatedEvent); + + const id = instantDistributionUpdatedEvent.params.pool.toHexString(); + + assertHigherOrderBaseProperties("Pool", id, instantDistributionUpdatedEvent); + assert.fieldEquals("Pool", id, "totalUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalConnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountInstantlyDistributedUntilUpdatedAt", requestedAmount.toString()); + assert.fieldEquals("Pool", id, "totalAmountFlowedDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountDistributedUntilUpdatedAt", requestedAmount.toString()); + assert.fieldEquals("Pool", id, "totalMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalConnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "adjustmentFlowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "flowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalBuffer", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "token", ZERO_ADDRESS.toHex()); + assert.fieldEquals("Pool", id, "admin", ZERO_ADDRESS.toHex()); + }); + + test("handleDistributionClaimed() - Pool Entity: No changes", () => { + const poolMember = alice; + const claimedAmount = BigInt.fromI32(100000000); + const distributionClaimedEvent = createDistributionClaimedEvent( + superToken, + poolMember, + claimedAmount, + claimedAmount + ); + + handleDistributionClaimed(distributionClaimedEvent); + + const id = distributionClaimedEvent.address.toHexString(); + + assertHigherOrderBaseProperties("Pool", id, distributionClaimedEvent); + assert.fieldEquals("Pool", id, "totalUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalConnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountInstantlyDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountFlowedDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalConnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "adjustmentFlowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "flowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalBuffer", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "token", ZERO_ADDRESS.toHex()); + assert.fieldEquals("Pool", id, "admin", ZERO_ADDRESS.toHex()); + }); + + test("handleMemberUnitsUpdated() - Pool Entity: Units data updated (connected member) 0 to > 0 units", () => { + const poolMember = alice; + const oldUnits = BigInt.fromI32(0); + const newUnits = BigInt.fromI32(100000000); + const memberUnitsUpdatedEvent = createMemberUnitsUpdatedEvent(superToken, poolMember, oldUnits, newUnits); + const poolMemberEntity = getOrInitPoolMember( + memberUnitsUpdatedEvent, + memberUnitsUpdatedEvent.address, + Address.fromString(poolMember) + ); + poolMemberEntity.isConnected = true; + poolMemberEntity.save(); + + handleMemberUnitsUpdated(memberUnitsUpdatedEvent); + + const id = memberUnitsUpdatedEvent.address.toHexString(); + + assertHigherOrderBaseProperties("Pool", id, memberUnitsUpdatedEvent); + assert.fieldEquals("Pool", id, "totalUnits", newUnits.toString()); + assert.fieldEquals("Pool", id, "totalConnectedUnits", newUnits.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountInstantlyDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountFlowedDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalMembers", BIG_INT_ONE.toString()); + assert.fieldEquals("Pool", id, "totalConnectedMembers", BIG_INT_ONE.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "adjustmentFlowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "flowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalBuffer", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "token", ZERO_ADDRESS.toHex()); + assert.fieldEquals("Pool", id, "admin", ZERO_ADDRESS.toHex()); + }); + + test("handleMemberUnitsUpdated() - Pool Entity: Units data updated (connected member) > 0 to 0 units", () => { + const poolMember = alice; + const oldUnits = BigInt.fromI32(0); + const newUnits = BigInt.fromI32(100000000); + const memberUnitsUpdatedEvent = createMemberUnitsUpdatedEvent(superToken, poolMember, oldUnits, newUnits); + const poolMemberEntity = getOrInitPoolMember( + memberUnitsUpdatedEvent, + memberUnitsUpdatedEvent.address, + Address.fromString(poolMember) + ); + poolMemberEntity.isConnected = true; + poolMemberEntity.save(); + + handleMemberUnitsUpdated(memberUnitsUpdatedEvent); + + const memberUnitsUpdatedEventZeroUnits = createMemberUnitsUpdatedEvent( + superToken, + poolMember, + newUnits, + BIG_INT_ZERO + ); + + handleMemberUnitsUpdated(memberUnitsUpdatedEventZeroUnits); + + const id = memberUnitsUpdatedEvent.address.toHexString(); + + assertHigherOrderBaseProperties("Pool", id, memberUnitsUpdatedEvent); + assert.fieldEquals("Pool", id, "totalUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalConnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountInstantlyDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountFlowedDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalConnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "adjustmentFlowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "flowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalBuffer", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "token", ZERO_ADDRESS.toHex()); + assert.fieldEquals("Pool", id, "admin", ZERO_ADDRESS.toHex()); + }); + + test("handleMemberUnitsUpdated() - Pool Entity: Units data updated (disconnected member) 0 to > 0 units", () => { + const poolMember = alice; + const oldUnits = BigInt.fromI32(0); + const newUnits = BigInt.fromI32(100000000); + const memberUnitsUpdatedEvent = createMemberUnitsUpdatedEvent(superToken, poolMember, oldUnits, newUnits); + const poolMemberEntity = getOrInitPoolMember( + memberUnitsUpdatedEvent, + memberUnitsUpdatedEvent.address, + Address.fromString(poolMember) + ); + poolMemberEntity.save(); + + handleMemberUnitsUpdated(memberUnitsUpdatedEvent); + + const id = memberUnitsUpdatedEvent.address.toHexString(); + + assertHigherOrderBaseProperties("Pool", id, memberUnitsUpdatedEvent); + assert.fieldEquals("Pool", id, "totalUnits", newUnits.toString()); + assert.fieldEquals("Pool", id, "totalConnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedUnits", newUnits.toString()); + assert.fieldEquals("Pool", id, "totalAmountInstantlyDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountFlowedDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalMembers", BIG_INT_ONE.toString()); + assert.fieldEquals("Pool", id, "totalConnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedMembers", BIG_INT_ONE.toString()); + assert.fieldEquals("Pool", id, "adjustmentFlowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "flowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalBuffer", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "token", ZERO_ADDRESS.toHex()); + assert.fieldEquals("Pool", id, "admin", ZERO_ADDRESS.toHex()); + }); + + test("handleMemberUnitsUpdated() - Pool Entity: Units data updated (connected member) > 0 to 0 units", () => { + const poolMember = alice; + const oldUnits = BigInt.fromI32(0); + const newUnits = BigInt.fromI32(100000000); + const memberUnitsUpdatedEvent = createMemberUnitsUpdatedEvent(superToken, poolMember, oldUnits, newUnits); + const poolMemberEntity = getOrInitPoolMember( + memberUnitsUpdatedEvent, + memberUnitsUpdatedEvent.address, + Address.fromString(poolMember) + ); + poolMemberEntity.save(); + + handleMemberUnitsUpdated(memberUnitsUpdatedEvent); + + const memberUnitsUpdatedEventZeroUnits = createMemberUnitsUpdatedEvent( + superToken, + poolMember, + newUnits, + BIG_INT_ZERO + ); + + handleMemberUnitsUpdated(memberUnitsUpdatedEventZeroUnits); + + const id = memberUnitsUpdatedEvent.address.toHexString(); + + assertHigherOrderBaseProperties("Pool", id, memberUnitsUpdatedEvent); + assert.fieldEquals("Pool", id, "totalUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalConnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountInstantlyDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountFlowedDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalConnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "adjustmentFlowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "flowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalBuffer", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "token", ZERO_ADDRESS.toHex()); + assert.fieldEquals("Pool", id, "admin", ZERO_ADDRESS.toHex()); + }); + + test("handlePoolConnectionUpdated - PoolMember Entity: isConnected updated from false to true", () => { + const account = bob; + const connected = true; + const oldUnits = BigInt.fromI32(0); + const newUnits = BigInt.fromI32(1); + const userData = stringToBytes(""); + + const memberUnitsUpdatedEvent = updateMemberUnitsAndReturnMemberUnitsUpdatedEvent( + superToken, + account, + oldUnits, + newUnits + ); + + const poolAddress = memberUnitsUpdatedEvent.address; + + const poolConnectionUpdatedEvent = updatePoolConnectionAndReturnPoolConnectionUpdatedEvent( + superToken, + account, + poolAddress.toHexString(), + connected, + initialFlowRate, + userData + ); + const id = getPoolMemberID(poolAddress, Address.fromString(account)); + + assertHigherOrderBaseProperties("PoolMember", id, poolConnectionUpdatedEvent); + assert.fieldEquals("PoolMember", id, "units", newUnits.toString()); + assert.fieldEquals("PoolMember", id, "isConnected", TRUE); + assert.fieldEquals("PoolMember", id, "totalAmountClaimed", BIG_INT_ZERO.toString()); + assert.fieldEquals("PoolMember", id, "account", account); + assert.fieldEquals("PoolMember", id, "pool", poolAddress.toHexString()); + }); + + test("handlePoolConnectionUpdated - PoolMember Entity: isConnected updated from true to false", () => { + const account = bob; + const connected = true; + const oldUnits = BigInt.fromI32(0); + const newUnits = BigInt.fromI32(1); + const userData = stringToBytes(""); + + const memberUnitsUpdatedEvent = updateMemberUnitsAndReturnMemberUnitsUpdatedEvent( + superToken, + account, + oldUnits, + newUnits + ); + + const poolAddress = memberUnitsUpdatedEvent.address; + + const poolConnectionUpdatedEvent = updatePoolConnectionAndReturnPoolConnectionUpdatedEvent( + superToken, + account, + poolAddress.toHexString(), + connected, + initialFlowRate, + userData + ); + + updatePoolConnectionAndReturnPoolConnectionUpdatedEvent( + superToken, + account, + poolAddress.toHexString(), + false, + initialFlowRate, + userData + ); + const id = getPoolMemberID(poolAddress, Address.fromString(account)); + + assertHigherOrderBaseProperties("PoolMember", id, poolConnectionUpdatedEvent); + assert.fieldEquals("PoolMember", id, "units", newUnits.toString()); + assert.fieldEquals("PoolMember", id, "isConnected", FALSE); + assert.fieldEquals("PoolMember", id, "totalAmountClaimed", BIG_INT_ZERO.toString()); + assert.fieldEquals("PoolMember", id, "account", account); + assert.fieldEquals("PoolMember", id, "pool", poolAddress.toHexString()); + }); + + test("handleDistributionClaimed() - PoolMember Entity: totalAmountClaimed updated", () => { + const poolMember = alice; + const claimedAmount = BigInt.fromI32(100000000); + const distributionClaimedEvent = createDistributionClaimedEvent( + superToken, + poolMember, + claimedAmount, + claimedAmount + ); + + const poolAddress = distributionClaimedEvent.address; + + handleDistributionClaimed(distributionClaimedEvent); + + const id = getPoolMemberID(poolAddress, Address.fromString(poolMember)); + + assertHigherOrderBaseProperties("PoolMember", id, distributionClaimedEvent); + assert.fieldEquals("PoolMember", id, "units", BIG_INT_ZERO.toString()); + assert.fieldEquals("PoolMember", id, "isConnected", FALSE); + assert.fieldEquals("PoolMember", id, "totalAmountClaimed", claimedAmount.toString()); + assert.fieldEquals("PoolMember", id, "account", poolMember); + assert.fieldEquals("PoolMember", id, "pool", poolAddress.toHexString()); + }); + + test("handleMemberUnitsUpdated() - PoolMember Entity: units updated", () => { + const poolMember = alice; + const oldUnits = BigInt.fromI32(0); + const newUnits = BigInt.fromI32(100000000); + const memberUnitsUpdatedEvent = createMemberUnitsUpdatedEvent(superToken, poolMember, oldUnits, newUnits); + + const poolAddress = memberUnitsUpdatedEvent.address; + + handleMemberUnitsUpdated(memberUnitsUpdatedEvent); + + const id = getPoolMemberID(poolAddress, Address.fromString(poolMember)); + + assertHigherOrderBaseProperties("PoolMember", id, memberUnitsUpdatedEvent); + assert.fieldEquals("PoolMember", id, "units", newUnits.toString()); + assert.fieldEquals("PoolMember", id, "isConnected", FALSE); + assert.fieldEquals("PoolMember", id, "totalAmountClaimed", BIG_INT_ZERO.toString()); + assert.fieldEquals("PoolMember", id, "account", poolMember); + assert.fieldEquals("PoolMember", id, "pool", poolAddress.toHexString()); + }); + + test("handleBufferAdjusted() - PoolDistributor Entity: totalBufferAmount updated", () => { + const distributor = alice; + + const BUFFER = BigInt.fromI32(100); + + const bufferAdjustedEvent = createBufferAdjustedEvent( + superToken, // token + superfluidPool, // pool + distributor, // poolDistributor + BUFFER, // bufferDelta + BUFFER, // newBufferAmount + BUFFER // totalBufferAmount + ); + + handleBufferAdjusted(bufferAdjustedEvent); + + const id = getPoolDistributorID(Address.fromString(superfluidPool), Address.fromString(distributor)); + + assertHigherOrderBaseProperties("PoolDistributor", id, bufferAdjustedEvent); + assert.fieldEquals( + "PoolDistributor", + id, + "totalAmountInstantlyDistributedUntilUpdatedAt", + BIG_INT_ZERO.toString() + ); + assert.fieldEquals( + "PoolDistributor", + id, + "totalAmountFlowedDistributedUntilUpdatedAt", + BIG_INT_ZERO.toString() + ); + assert.fieldEquals("PoolDistributor", id, "totalAmountDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("PoolDistributor", id, "totalBuffer", BUFFER.toString()); + assert.fieldEquals("PoolDistributor", id, "flowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("PoolDistributor", id, "account", distributor); + assert.fieldEquals("PoolDistributor", id, "pool", superfluidPool); + }); + + test("handleFlowDistributionUpdated() - PoolDistributor Entity: flowRate updated", () => { + const distributor = alice; + const operator = alice; + const emptyFlowRate = BigInt.fromI32(0); + const newFlowRate = BigInt.fromI32(100000000); + const userData = stringToBytes(""); + const flowDistributionUpdatedEvent = createFlowDistributionUpdatedEvent( + superToken, + superfluidPool, + distributor, + operator, + emptyFlowRate, // old flow rate + newFlowRate, // new distributor to pool flow rate + newFlowRate, // new total distribution flow rate + alice, // adjustment flow recipient + BigInt.fromI32(0), // adjustment flow rate + userData + ); + + handleFlowDistributionUpdated(flowDistributionUpdatedEvent); + + const id = getPoolDistributorID(Address.fromString(superfluidPool), Address.fromString(distributor)); + + assertHigherOrderBaseProperties("PoolDistributor", id, flowDistributionUpdatedEvent); + assert.fieldEquals( + "PoolDistributor", + id, + "totalAmountInstantlyDistributedUntilUpdatedAt", + BIG_INT_ZERO.toString() + ); + assert.fieldEquals( + "PoolDistributor", + id, + "totalAmountFlowedDistributedUntilUpdatedAt", + BIG_INT_ZERO.toString() + ); + assert.fieldEquals("PoolDistributor", id, "totalAmountDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("PoolDistributor", id, "totalBuffer", BIG_INT_ZERO.toString()); + assert.fieldEquals("PoolDistributor", id, "flowRate", newFlowRate.toString()); + assert.fieldEquals("PoolDistributor", id, "account", distributor); + assert.fieldEquals("PoolDistributor", id, "pool", superfluidPool); + }); + + test("handleInstantDistributionUpdated() - PoolDistributor Entity: flowRate updated", () => { + const distributor = alice; + const operator = alice; + const requestedAmount = BigInt.fromI32(100000000); + const userData = stringToBytes(""); + const instantDistributionUpdatedEvent = createInstantDistributionUpdatedEvent( + superToken, + superfluidPool, + distributor, + operator, + requestedAmount, + requestedAmount, + userData + ); + + handleInstantDistributionUpdated(instantDistributionUpdatedEvent); + + const id = getPoolDistributorID(Address.fromString(superfluidPool), Address.fromString(distributor)); + + assertHigherOrderBaseProperties("PoolDistributor", id, instantDistributionUpdatedEvent); + assert.fieldEquals( + "PoolDistributor", + id, + "totalAmountInstantlyDistributedUntilUpdatedAt", + requestedAmount.toString() + ); + assert.fieldEquals( + "PoolDistributor", + id, + "totalAmountFlowedDistributedUntilUpdatedAt", + BIG_INT_ZERO.toString() + ); + assert.fieldEquals("PoolDistributor", id, "totalAmountDistributedUntilUpdatedAt", requestedAmount.toString()); + assert.fieldEquals("PoolDistributor", id, "totalBuffer", BIG_INT_ZERO.toString()); + assert.fieldEquals("PoolDistributor", id, "flowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("PoolDistributor", id, "account", distributor); + assert.fieldEquals("PoolDistributor", id, "pool", superfluidPool); + }); +}); + +function assertEmptyPoolData(id: string, event: ethereum.Event, token: string): void { + assertHigherOrderBaseProperties("Pool", id, event); + assert.fieldEquals("Pool", id, "totalUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalConnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedUnits", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountInstantlyDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountFlowedDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalAmountDistributedUntilUpdatedAt", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalConnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalDisconnectedMembers", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "adjustmentFlowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "flowRate", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "totalBuffer", BIG_INT_ZERO.toString()); + assert.fieldEquals("Pool", id, "token", token); + assert.fieldEquals("Pool", id, "admin", admin); +} diff --git a/packages/subgraph/tests/resolver/resolver.test.ts b/packages/subgraph/tests/resolver/resolver.test.ts index c7a17b7f10..082e9c8b5c 100644 --- a/packages/subgraph/tests/resolver/resolver.test.ts +++ b/packages/subgraph/tests/resolver/resolver.test.ts @@ -1,23 +1,7 @@ import { newMockEvent } from "matchstick-as"; -import { - assert, - beforeEach, - clearStore, - describe, - test, -} from "matchstick-as/assembly/index"; -import { - DEFAULT_DECIMALS, - FALSE, - maticXAddress, - maticXName, - maticXSymbol, - TRUE, -} from "../constants"; -import { - assertEventBaseProperties, - assertHigherOrderBaseProperties, -} from "../assertionHelpers"; +import { assert, beforeEach, clearStore, describe, test } from "matchstick-as/assembly/index"; +import { DEFAULT_DECIMALS, FALSE, maticXAddress, maticXName, maticXSymbol, TRUE } from "../constants"; +import { assertEventBaseProperties, assertHigherOrderBaseProperties } from "../assertionHelpers"; import { createSetEvent } from "./resolver.helper"; import { handleSet } from "../../src/mappings/resolver"; import { stringToBytes } from "../converters"; @@ -30,10 +14,7 @@ import { Address } from "@graphprotocol/graph-ts"; * @param target the target address * @returns ResolverEntry id */ -function testResolverEntryParams( - tokenAddress: Address, - target: Address -): string { +function testResolverEntryParams(tokenAddress: Address, target: Address): string { const name = stringToBytes("supertokens.v1.maticx"); const setEvent = createSetEvent(name, target.toHexString()); const isToken = tokenAddress.equals(Address.zero()) ? FALSE : TRUE; @@ -42,20 +23,8 @@ function testResolverEntryParams( handleSet(setEvent); const resolverEntryId = name.toHex(); - assertHigherOrderBaseProperties( - "ResolverEntry", - resolverEntryId, - setEvent.block.timestamp, - setEvent.block.number, - setEvent.block.timestamp, - setEvent.block.number - ); - assert.fieldEquals( - "ResolverEntry", - resolverEntryId, - "targetAddress", - target.toHexString() - ); + assertHigherOrderBaseProperties("ResolverEntry", resolverEntryId, setEvent); + assert.fieldEquals("ResolverEntry", resolverEntryId, "targetAddress", target.toHexString()); assert.fieldEquals("ResolverEntry", resolverEntryId, "isToken", isToken); assert.fieldEquals("ResolverEntry", resolverEntryId, "isListed", isListed); @@ -76,19 +45,9 @@ describe("Resolver Mapper Unit Tests", () => { handleSet(setEvent); const id = assertEventBaseProperties(setEvent, "Set"); - assert.fieldEquals( - "SetEvent", - id, - "hashedName", - name.toHexString() - ); + assert.fieldEquals("SetEvent", id, "hashedName", name.toHexString()); assert.fieldEquals("SetEvent", id, "target", target); - assert.fieldEquals( - "SetEvent", - id, - "resolverEntry", - name.toHexString() - ); + assert.fieldEquals("SetEvent", id, "resolverEntry", name.toHexString()); }); }); @@ -110,15 +69,7 @@ describe("Resolver Mapper Unit Tests", () => { test("Should create a ResolverEntry entity (token case) - list case", () => { const mockEvent = newMockEvent(); const token = Address.fromString(maticXAddress); - createSuperToken( - token, - mockEvent.block, - DEFAULT_DECIMALS, - maticXName, - maticXSymbol, - false, - Address.zero() - ); + createSuperToken(token, mockEvent.block, DEFAULT_DECIMALS, maticXName, maticXSymbol, false, Address.zero()); const target = Address.fromString(maticXAddress); assert.fieldEquals("Token", maticXAddress, "isListed", FALSE); // list token on resolver diff --git a/packages/subgraph/tests/superToken/event/superToken.event.test.ts b/packages/subgraph/tests/superToken/event/superToken.event.test.ts index a29a9ac385..4a335bd4c0 100644 --- a/packages/subgraph/tests/superToken/event/superToken.event.test.ts +++ b/packages/subgraph/tests/superToken/event/superToken.event.test.ts @@ -17,10 +17,7 @@ import { handleTransfer, } from "../../../src/mappings/superToken"; import { BIG_INT_ONE, BIG_INT_ZERO, encode, ZERO_ADDRESS } from "../../../src/utils"; -import { - assertEventBaseProperties, - assertTokenStatisticProperties, -} from "../../assertionHelpers"; +import { assertEmptyTokenStatisticProperties, assertEventBaseProperties, assertTokenStatisticProperties } from "../../assertionHelpers"; import { alice, bob, cfaV1Address, charlie, DEFAULT_DECIMALS, delta, FAKE_INITIAL_BALANCE, maticXName, maticXSymbol } from "../../constants"; import { getETHAddress, getETHUnsignedBigInt, stringToBytes } from "../../converters"; import { createStream, createStreamRevision } from "../../mockedEntities"; @@ -273,14 +270,24 @@ describe("SuperToken Mapper Unit Tests", () => { agreementLiquidatedV2Event.block.timestamp, agreementLiquidatedV2Event.block.number, 0, // totalNumberOfActiveStreams + 0, // totalCFANumberOfActiveStreams + 0, // totalGDANumberOfActiveStreams 0, // totalNumberOfClosedStreams + 0, // totalCFANumberOfClosedStreams + 0, // totalGDANumberOfClosedStreams 0, // totalNumberOfIndexes 0, // totalNumberOfActiveIndexes 0, // totalSubscriptionsWithUnits 0, // totalApprovedSubscriptions BIG_INT_ZERO, // totalDeposit + BIG_INT_ZERO, // totalCFADeposit + BIG_INT_ZERO, // totalGDADeposit BIG_INT_ZERO, // totalOutflowRate + BIG_INT_ZERO, // totalCFAOutflowRate + BIG_INT_ZERO, // totalGDAOutflowRate BIG_INT_ZERO, // totalAmountStreamedUntilUpdatedAt + BIG_INT_ZERO, // totalCFAAmountStreamedUntilUpdatedAt + BIG_INT_ZERO, // totalGDAAmountStreamedUntilUpdatedAt BIG_INT_ZERO, // totalAmountTransferredUntilUpdatedAt BIG_INT_ZERO, // totalAmountDistributedUntilUpdatedAt BigInt.fromI32(1000000), // totalSupply = 100 @@ -358,14 +365,24 @@ describe("SuperToken Mapper Unit Tests", () => { transferEvent.block.timestamp, transferEvent.block.number, 0, // totalNumberOfActiveStreams + 0, // totalCFANumberOfActiveStreams + 0, // totalGDANumberOfActiveStreams 0, // totalNumberOfClosedStreams + 0, // totalCFANumberOfClosedStreams + 0, // totalGDANumberOfClosedStreams 0, // totalNumberOfIndexes 0, // totalNumberOfActiveIndexes 0, // totalSubscriptionsWithUnits 0, // totalApprovedSubscriptions BIG_INT_ZERO, // totalDeposit + BIG_INT_ZERO, // totalCFADeposit + BIG_INT_ZERO, // totalGDADeposit BIG_INT_ZERO, // totalOutflowRate + BIG_INT_ZERO, // totalCFAOutflowRate + BIG_INT_ZERO, // totalGDAOutflowRate BIG_INT_ZERO, // totalAmountStreamedUntilUpdatedAt + BIG_INT_ZERO, // totalCFAAmountStreamedUntilUpdatedAt + BIG_INT_ZERO, // totalGDAAmountStreamedUntilUpdatedAt value, // totalAmountTransferredUntilUpdatedAt BIG_INT_ZERO, // totalAmountDistributedUntilUpdatedAt BigInt.fromI32(1000000), // totalSupply = 100 @@ -495,14 +512,24 @@ describe("SuperToken Mapper Unit Tests", () => { transferEvent.block.timestamp, transferEvent.block.number, 0, // totalNumberOfActiveStreams + 0, // totalCFANumberOfActiveStreams + 0, // totalGDANumberOfActiveStreams 0, // totalNumberOfClosedStreams + 0, // totalCFANumberOfClosedStreams + 0, // totalGDANumberOfClosedStreams 0, // totalNumberOfIndexes 0, // totalNumberOfActiveIndexes 0, // totalSubscriptionsWithUnits 0, // totalApprovedSubscriptions BIG_INT_ZERO, // totalDeposit + BIG_INT_ZERO, // totalCFADeposit + BIG_INT_ZERO, // totalGDADeposit BIG_INT_ZERO, // totalOutflowRate + BIG_INT_ZERO, // totalCFAOutflowRate + BIG_INT_ZERO, // totalGDAOutflowRate BIG_INT_ZERO, // totalAmountStreamedUntilUpdatedAt + BIG_INT_ZERO, // totalCFAAmountStreamedUntilUpdatedAt + BIG_INT_ZERO, // totalGDAAmountStreamedUntilUpdatedAt value, // totalAmountTransferredUntilUpdatedAt BIG_INT_ZERO, // totalAmountDistributedUntilUpdatedAt BigInt.fromI32(1000000), // totalSupply = 100 @@ -535,14 +562,24 @@ describe("SuperToken Mapper Unit Tests", () => { secondTransferEvent.block.timestamp, secondTransferEvent.block.number, 0, // totalNumberOfActiveStreams + 0, // totalCFANumberOfActiveStreams + 0, // totalGDANumberOfActiveStreams 0, // totalNumberOfClosedStreams + 0, // totalCFANumberOfClosedStreams + 0, // totalGDANumberOfClosedStreams 0, // totalNumberOfIndexes 0, // totalNumberOfActiveIndexes 0, // totalSubscriptionsWithUnits 0, // totalApprovedSubscriptions BIG_INT_ZERO, // totalDeposit + BIG_INT_ZERO, // totalCFADeposit + BIG_INT_ZERO, // totalGDADeposit BIG_INT_ZERO, // totalOutflowRate + BIG_INT_ZERO, // totalCFAOutflowRate + BIG_INT_ZERO, // totalGDAOutflowRate BIG_INT_ZERO, // totalAmountStreamedUntilUpdatedAt + BIG_INT_ZERO, // totalCFAAmountStreamedUntilUpdatedAt + BIG_INT_ZERO, // totalGDAAmountStreamedUntilUpdatedAt value.times(BigInt.fromI32(2)), // totalAmountTransferredUntilUpdatedAt BIG_INT_ZERO, // totalAmountDistributedUntilUpdatedAt BigInt.fromI32(1000000), // totalSupply = 100 @@ -573,26 +610,13 @@ describe("SuperToken Mapper Unit Tests", () => { ); handleBurned(burnedEvent); - assertTokenStatisticProperties( + assertEmptyTokenStatisticProperties( null, null, burnedEvent.address.toHex(), burnedEvent.block.timestamp, burnedEvent.block.number, - 0, // totalNumberOfActiveStreams - 0, // totalNumberOfClosedStreams - 0, // totalNumberOfIndexes - 0, // totalNumberOfActiveIndexes - 0, // totalSubscriptionsWithUnits - 0, // totalApprovedSubscriptions - BIG_INT_ZERO, // totalDeposit - BIG_INT_ZERO, // totalOutflowRate - BIG_INT_ZERO, // totalAmountStreamedUntilUpdatedAt - BIG_INT_ZERO, // totalAmountTransferredUntilUpdatedAt - BIG_INT_ZERO, // totalAmountDistributedUntilUpdatedAt - amount.neg(), // totalSupply = -100 (not possible in practice) - 0, // totalNumberOfAccounts - 0, // totalNumberOfHolders + amount.neg() // totalSupply = -100 (not possible in practice) ); }); @@ -612,26 +636,13 @@ describe("SuperToken Mapper Unit Tests", () => { ); handleMinted(mintedEvent); - assertTokenStatisticProperties( + assertEmptyTokenStatisticProperties( null, null, mintedEvent.address.toHex(), mintedEvent.block.timestamp, mintedEvent.block.number, - 0, // totalNumberOfActiveStreams - 0, // totalNumberOfClosedStreams - 0, // totalNumberOfIndexes - 0, // totalNumberOfActiveIndexes - 0, // totalSubscriptionsWithUnits - 0, // totalApprovedSubscriptions - BIG_INT_ZERO, // totalDeposit - BIG_INT_ZERO, // totalOutflowRate - BIG_INT_ZERO, // totalAmountStreamedUntilUpdatedAt - BIG_INT_ZERO, // totalAmountTransferredUntilUpdatedAt - BIG_INT_ZERO, // totalAmountDistributedUntilUpdatedAt - amount, // totalSupply = 100 - 0, // totalNumberOfAccounts, - 0 // totalNumberOfHolders + amount // totalSupply = 100 ); }); }); diff --git a/packages/subgraph/tests/superToken/hol/supertoken.hol.test.ts b/packages/subgraph/tests/superToken/hol/supertoken.hol.test.ts index b71e162e1f..79362bc236 100644 --- a/packages/subgraph/tests/superToken/hol/supertoken.hol.test.ts +++ b/packages/subgraph/tests/superToken/hol/supertoken.hol.test.ts @@ -1,19 +1,9 @@ import { assert, beforeEach, clearStore, describe, test } from "matchstick-as"; -import { - createFlowOperatorUpdatedEvent, -} from "../../cfav1/cfav1.helper"; -import { - alice, - bob, - maticXAddress, -} from "../../constants"; -import { - BIG_INT_ZERO, - getAccountTokenSnapshotID, - getFlowOperatorID, -} from "../../../src/utils"; +import { createFlowOperatorUpdatedEvent } from "../../cfav1/cfav1.helper"; +import { alice, bob, maticXAddress } from "../../constants"; +import { BIG_INT_ZERO, getAccountTokenSnapshotID, getFlowOperatorID } from "../../../src/utils"; import { Address, BigInt } from "@graphprotocol/graph-ts"; -import {mockedApprove, mockedGetAppManifest} from "../../mockedFunctions"; +import { mockedApprove, mockedGetAppManifest } from "../../mockedFunctions"; import { handleFlowOperatorUpdated } from "../../../src/mappings/cfav1"; import { handleApproval } from "../../../src/mappings/superToken"; import { assertHigherOrderBaseProperties } from "../../assertionHelpers"; @@ -53,35 +43,17 @@ describe("SuperToken Higher Order Level Entity Unit Tests", () => { assert.fieldEquals("FlowOperator", id, "allowance", "0"); // trigger approve event - const approvalEvent = createApprovalEvent( - superToken, - sender, - flowOperator, - allowance - ); + const approvalEvent = createApprovalEvent(superToken, sender, flowOperator, allowance); handleApproval(approvalEvent); - const atsId = getAccountTokenSnapshotID( - Address.fromString(sender), - Address.fromString(superToken) - ); - assertHigherOrderBaseProperties( - "FlowOperator", - id, - flowOperatorUpdatedEvent.block.timestamp, - flowOperatorUpdatedEvent.block.number, - flowOperatorUpdatedEvent.block.timestamp, - flowOperatorUpdatedEvent.block.number - ); - assert.fieldEquals("FlowOperator", id, "permissions", permissions.toString() - ); + const atsId = getAccountTokenSnapshotID(Address.fromString(sender), Address.fromString(superToken)); + assertHigherOrderBaseProperties("FlowOperator", id, flowOperatorUpdatedEvent); + assert.fieldEquals("FlowOperator", id, "permissions", permissions.toString()); assert.fieldEquals("FlowOperator", id, "flowRateAllowanceGranted", flowRateAllowance.toString()); - assert.fieldEquals("FlowOperator", id, "flowRateAllowanceRemaining", flowRateAllowance.toString() - ); + assert.fieldEquals("FlowOperator", id, "flowRateAllowanceRemaining", flowRateAllowance.toString()); assert.fieldEquals("FlowOperator", id, "flowOperator", flowOperator); - assert.fieldEquals("FlowOperator", id, "allowance", allowance.toString() - ); + assert.fieldEquals("FlowOperator", id, "allowance", allowance.toString()); assert.fieldEquals("FlowOperator", id, "sender", sender); assert.fieldEquals("FlowOperator", id, "token", superToken); assert.fieldEquals("FlowOperator", id, "accountTokenSnapshot", atsId); diff --git a/packages/subgraph/tests/superTokenFactory/superTokenFactory.test.ts b/packages/subgraph/tests/superTokenFactory/superTokenFactory.test.ts index 9a2e146448..f070724e68 100644 --- a/packages/subgraph/tests/superTokenFactory/superTokenFactory.test.ts +++ b/packages/subgraph/tests/superTokenFactory/superTokenFactory.test.ts @@ -10,7 +10,7 @@ import { handleSuperTokenCreated, handleSuperTokenLogicCreated, } from "../../src/mappings/superTokenFactory"; -import { assertEventBaseProperties, assertTokenStatisticProperties } from "../assertionHelpers"; +import { assertEmptyTokenStatisticProperties, assertEventBaseProperties } from "../assertionHelpers"; import { daiAddress, daiName, @@ -227,26 +227,13 @@ describe("SuperTokenFactory Mapper Unit Tests", () => { handleSuperTokenCreated(SuperTokenCreatedEvent); // Validate Created TokenStatistic properties - assertTokenStatisticProperties( + assertEmptyTokenStatisticProperties( SuperTokenCreatedEvent, "SuperTokenCreated", maticXAddress, SuperTokenCreatedEvent.block.timestamp, SuperTokenCreatedEvent.block.number, - 0, // totalNumberOfActiveStreams - 0, // totalNumberOfClosedStreams - 0, // totalNumberOfIndexes - 0, // totalNumberOfActiveIndexes - 0, // totalSubscriptionsWithUnits - 0, // totalApprovedSubscriptions - BIG_INT_ZERO, // totalDeposit - BIG_INT_ZERO, // totalOutflowRate - BIG_INT_ZERO, // totalAmountStreamedUntilUpdatedAt - BIG_INT_ZERO, // totalAmountTransferredUntilUpdatedAt - BIG_INT_ZERO, // totalAmountDistributedUntilUpdatedAt - FAKE_SUPER_TOKEN_TOTAL_SUPPLY, // totalSupply = 100, - 0, // totalNumberOfAccounts - 0 // totalNumberOfHolders + FAKE_SUPER_TOKEN_TOTAL_SUPPLY // totalSupply = 100 ); }); diff --git a/yarn.lock b/yarn.lock index d51fe41433..4cc83819f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2630,7 +2630,7 @@ "@nomicfoundation/solidity-analyzer-win32-ia32-msvc" "0.1.1" "@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.1.1" -"@nomiclabs/hardhat-ethers@^2.2.1", "@nomiclabs/hardhat-ethers@^2.2.3": +"@nomiclabs/hardhat-ethers@^2.2.3": version "2.2.3" resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.2.3.tgz#b41053e360c31a32c2640c9a45ee981a7e603fe0" integrity sha512-YhzPdzb612X591FOe68q+qXVXGG2ANZRvDo0RRUtimev85rCrAlv/TLMEZw5c+kq9AbzocLTVX/h2jVIFPL9Xg== @@ -3027,11 +3027,6 @@ find-up "^4.1.0" fs-extra "^8.1.0" -"@openzeppelin/contracts@4.8.2": - version "4.8.2" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.2.tgz#d815ade0027b50beb9bcca67143c6bcc3e3923d6" - integrity sha512-kEUOgPQszC0fSYWpbh2kT94ltOJwj1qfT2DWo+zVttmGmf97JZ99LspePNaeeaLhCImaHVeBbjaQFZQn7+Zc5g== - "@openzeppelin/contracts@4.9.3": version "4.9.3" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.3.tgz#00d7a8cf35a475b160b3f0293a6403c511099364" @@ -3524,37 +3519,6 @@ dependencies: antlr4ts "^0.5.0-alpha.4" -"@superfluid-finance/ethereum-contracts@1.7.1": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@superfluid-finance/ethereum-contracts/-/ethereum-contracts-1.7.1.tgz#e2f08bed42694e94980199607ed9d3222faeb477" - integrity sha512-MPimKMSbvJOUkbMzGA2oNdGZrKpJnja3OGS4NdsRO+OxBya2XOTvoy55nb3H/u63IFagoKt6L7eSoJapKtjDrA== - dependencies: - "@decentral.ee/web3-helpers" "0.5.3" - "@openzeppelin/contracts" "4.8.2" - "@superfluid-finance/js-sdk" "0.6.3" - "@truffle/contract" "4.6.18" - ethereumjs-tx "2.1.2" - ethereumjs-util "7.1.5" - stack-trace "0.0.10" - -"@superfluid-finance/metadata@1.1.10": - version "1.1.10" - resolved "https://registry.yarnpkg.com/@superfluid-finance/metadata/-/metadata-1.1.10.tgz#980991d60066f21646d29eb01a9080c7fae1493d" - integrity sha512-IbcpfB/pOwjl/Vam0d1WXNJaeA0bUW/CkQEZlEhUpL+DQh01d6TnxneEjw3VsT9alqamtycKoi6+2uPHAzyvFA== - -"@superfluid-finance/sdk-core@0.6.8": - version "0.6.8" - resolved "https://registry.yarnpkg.com/@superfluid-finance/sdk-core/-/sdk-core-0.6.8.tgz#9ca45546cab97de47eb7e8f4ea190bd7a62fc440" - integrity sha512-OoID1Hmu3OJxXmU7P8+VFsU1sFw587Bak27pzwltm64Te7v0lY7HutcOUL4LaHFmqWhaxKoKqwDPNodxU4hVHA== - dependencies: - "@nomiclabs/hardhat-ethers" "^2.2.1" - "@superfluid-finance/ethereum-contracts" "1.7.1" - "@superfluid-finance/metadata" "1.1.10" - browserify "^17.0.0" - graphql-request "^4.3.0" - lodash "^4.17.21" - tsify "^5.0.4" - "@szmarczak/http-timer@^4.0.5": version "4.0.6" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" @@ -3764,26 +3728,6 @@ debug "^4.3.1" glob "^7.1.6" -"@truffle/contract@4.6.18", "@truffle/contract@^4.0.35", "@truffle/contract@^4.6.18": - version "4.6.18" - resolved "https://registry.yarnpkg.com/@truffle/contract/-/contract-4.6.18.tgz#096f82dbc05060acc9ed0bd8bb5811f497b8e3ad" - integrity sha512-x49EWZI16VMdYV8pH2LYM1AMFM3xAZ6ZFT2dG9Y71nIDZHdh+HKdlPSL40CqFtzpeoEk9UQoSJL99D/DXtpaog== - dependencies: - "@ensdomains/ensjs" "^2.1.0" - "@truffle/blockchain-utils" "^0.1.7" - "@truffle/contract-schema" "^3.4.13" - "@truffle/debug-utils" "^6.0.47" - "@truffle/error" "^0.2.0" - "@truffle/interface-adapter" "^0.5.31" - bignumber.js "^7.2.1" - debug "^4.3.1" - ethers "^4.0.32" - web3 "1.8.2" - web3-core-helpers "1.8.2" - web3-core-promievent "1.8.2" - web3-eth-abi "1.8.2" - web3-utils "1.8.2" - "@truffle/contract@4.6.29": version "4.6.29" resolved "https://registry.yarnpkg.com/@truffle/contract/-/contract-4.6.29.tgz#c1f0b9f65985ba5d8f35626a612dd31205cfcd6b" @@ -3804,6 +3748,26 @@ web3-eth-abi "1.10.0" web3-utils "1.10.0" +"@truffle/contract@^4.0.35", "@truffle/contract@^4.6.18": + version "4.6.18" + resolved "https://registry.yarnpkg.com/@truffle/contract/-/contract-4.6.18.tgz#096f82dbc05060acc9ed0bd8bb5811f497b8e3ad" + integrity sha512-x49EWZI16VMdYV8pH2LYM1AMFM3xAZ6ZFT2dG9Y71nIDZHdh+HKdlPSL40CqFtzpeoEk9UQoSJL99D/DXtpaog== + dependencies: + "@ensdomains/ensjs" "^2.1.0" + "@truffle/blockchain-utils" "^0.1.7" + "@truffle/contract-schema" "^3.4.13" + "@truffle/debug-utils" "^6.0.47" + "@truffle/error" "^0.2.0" + "@truffle/interface-adapter" "^0.5.31" + bignumber.js "^7.2.1" + debug "^4.3.1" + ethers "^4.0.32" + web3 "1.8.2" + web3-core-helpers "1.8.2" + web3-core-promievent "1.8.2" + web3-eth-abi "1.8.2" + web3-utils "1.8.2" + "@truffle/dashboard-message-bus-client@^0.1.10": version "0.1.10" resolved "https://registry.yarnpkg.com/@truffle/dashboard-message-bus-client/-/dashboard-message-bus-client-0.1.10.tgz#bd1cef19956f06716d55a327b8ea6f983e41f0b0" @@ -10604,15 +10568,6 @@ graphql-import-node@^0.0.5: resolved "https://registry.yarnpkg.com/graphql-import-node/-/graphql-import-node-0.0.5.tgz#caf76a6cece10858b14f27cce935655398fc1bf0" integrity sha512-OXbou9fqh9/Lm7vwXT0XoRN9J5+WCYKnbiTalgFDvkQERITRmcfncZs6aVABedd5B85yQU5EULS4a5pnbpuI0Q== -graphql-request@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-4.3.0.tgz#b934e08fcae764aa2cdc697d3c821f046cb5dbf2" - integrity sha512-2v6hQViJvSsifK606AliqiNiijb1uwWp6Re7o0RTyH+uRTv/u7Uqm2g4Fjq/LgZIzARB38RZEvVBFOQOVdlBow== - dependencies: - cross-fetch "^3.1.5" - extract-files "^9.0.0" - form-data "^3.0.0" - graphql-request@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-5.2.0.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