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 0000000000..0cd80588ff Binary files /dev/null and b/packages/ethereum-contracts/audits/Superfluid - Finance GDA - Comprehensive Report with Fix Review.pdf differ 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 ea051bbc8e..5735394014 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.tgz#a05fb54a517d91bb2d7aefa17ade4523dc5ebdca"