diff --git a/packages/automation-contracts/autowrap/foundry.toml b/packages/automation-contracts/autowrap/foundry.toml index e79af10c38..1f6a37e505 100644 --- a/packages/automation-contracts/autowrap/foundry.toml +++ b/packages/automation-contracts/autowrap/foundry.toml @@ -3,7 +3,7 @@ root = '../../../' libs = ['lib'] src = 'packages/automation-contracts/autowrap' solc_version = "0.8.30" -evm_version = 'paris' +evm_version = 'shanghai' optimizer = true optimizer_runs = 200 remappings = [ diff --git a/packages/automation-contracts/scheduler/foundry.toml b/packages/automation-contracts/scheduler/foundry.toml index a4e9c41259..102964fdb5 100644 --- a/packages/automation-contracts/scheduler/foundry.toml +++ b/packages/automation-contracts/scheduler/foundry.toml @@ -3,7 +3,7 @@ root = '../../../' libs = ['lib'] src = 'packages/automation-contracts/scheduler' solc_version = "0.8.30" -evm_version = 'paris' +evm_version = 'shanghai' optimizer = true optimizer_runs = 200 remappings = [ diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index 2fa1f22d73..30ab0e7116 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -5,12 +5,16 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [UNRELEASED] +### Added +- GDA _autoconnect_ feature: now any account can connect pool members using `tryConnectPoolFor()` as long as they have less than 4 connection slots occupied for that Super Token. This allows for smoother onboarding of new users, allowing Apps to make sure tokens distributed via GDA immediately show up in user's wallets. Accounts can opt out of this by using `setConnectPermission()`, this is mainly supposed to be used by contracts. + ### Changed - Refactored `GeneralDistributionAgreementV1`: extracted functionality which reads/writes agreement data from/to the token contract into dedicated libraries: - `GDAv1StorageLib` contains data structures and related encoders/decoders. - `GDAv1StorageReader` contains getters reading agreement data from the token contract, allowing contracts to get this data without making a call to the GDA contract. - `GDAv1StorageWriter` contains functions for writing agreement data to the token contract. This can only be used by the GDA contract itself. - bump solc to "0.8.30". +- Changed EVM target from `paris` to `shanghai` because now all networks with supported Superfluid deployment support it. ### Fixed - `ISuperfluidPool`: `getClaimable` and `getClaimableNow` could previously return non-zero values for connected pools, which was inconsistent with what `claimAll` would actually do in this situation (claim nothing). diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index 2ecb3e7671..08b23cde33 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.23; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import { ISuperfluid, ISuperfluidGovernance } from "../../interfaces/superfluid/ISuperfluid.sol"; +import { ISuperfluid, ISuperfluidGovernance, IAccessControl } from "../../interfaces/superfluid/ISuperfluid.sol"; import { BasicParticle, PDPoolIndex, @@ -23,8 +23,6 @@ import { } from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; import { SuperfluidUpgradeableBeacon } from "../../upgradability/SuperfluidUpgradeableBeacon.sol"; import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.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"; @@ -48,6 +46,12 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi address public constant SUPERFLUID_POOL_DEPLOYER_ADDRESS = address(SuperfluidPoolDeployerLibrary); + // @dev The max number of slots which can be used for connecting pools on behalf of a member (per token) + uint32 public constant MAX_POOL_AUTO_CONNECT_SLOTS = 4; + + // @dev The ACL role owned by this contract, used to persist autoconnect permissions for accounts + bytes32 constant public ACL_POOL_CONNECT_EXCLUSIVE_ROLE = keccak256("ACL_POOL_CONNECT_EXCLUSIVE_ROLE"); + /// @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 @@ -228,7 +232,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi function _createPool( ISuperfluidToken token, address admin, - PoolConfig memory config, + PoolConfig calldata config, PoolERC20Metadata memory poolERC20Metadata ) internal returns (ISuperfluidPool pool) { // @note ensure if token and admin are the same that nothing funky happens with echidna @@ -245,17 +249,13 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi token.setIsPoolFlag(pool); - IPoolAdminNFT poolAdminNFT = IPoolAdminNFT(_getPoolAdminNFTAddress(token)); - - if (address(poolAdminNFT) != address(0)) { - poolAdminNFT.mint(address(pool)); - } + SuperfluidPoolDeployerLibrary.mintPoolAdminNFT(token, pool); emit PoolCreated(token, admin, pool); } /// @inheritdoc IGeneralDistributionAgreementV1 - function createPool(ISuperfluidToken token, address admin, PoolConfig memory config) + function createPool(ISuperfluidToken token, address admin, PoolConfig calldata config) external override returns (ISuperfluidPool pool) @@ -272,7 +272,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi function createPoolWithCustomERC20Metadata( ISuperfluidToken token, address admin, - PoolConfig memory config, + PoolConfig calldata config, PoolERC20Metadata memory poolERC20Metadata ) external override returns (ISuperfluidPool pool) { return _createPool(token, admin, config, poolERC20Metadata); @@ -305,49 +305,106 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi pool.claimAll(memberAddress); } - // @note setPoolConnection function naming - function connectPool(ISuperfluidPool pool, bool doConnect, bytes calldata ctx) - public - returns (bytes memory newCtx) + /// @inheritdoc IGeneralDistributionAgreementV1 + function connectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) { + newCtx = ctx; + _setPoolConnectionFor(pool, address(0), true /* doConnect */, ctx); + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function tryConnectPoolFor(ISuperfluidPool pool, address memberAddr, bytes calldata ctx) + external + override + returns (bool success, bytes memory newCtx) + { + newCtx = ctx; + + if (pool.superToken().isPool(this, memberAddr)) { + revert GDA_CANNOT_CONNECT_POOL(); + } + + // check if the member has opted out of autoconnect + IAccessControl simpleACL = ISuperfluid(_host).getSimpleACL(); + if (simpleACL.hasRole(ACL_POOL_CONNECT_EXCLUSIVE_ROLE, memberAddr)) { + success = false; + } else { + success = _setPoolConnectionFor(pool, memberAddr, true /* doConnect */, ctx); + } + } + + function setConnectPermission(bool allow) external override { + IAccessControl simpleACL = ISuperfluid(_host).getSimpleACL(); + if (!allow) { + simpleACL.grantRole(ACL_POOL_CONNECT_EXCLUSIVE_ROLE, msg.sender); + } else { + simpleACL.revokeRole(ACL_POOL_CONNECT_EXCLUSIVE_ROLE, msg.sender); + } + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function disconnectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) { + newCtx = ctx; + _setPoolConnectionFor(pool, address(0), false /* doConnect */, ctx); + } + + // @note memberAddr has override semantics - if set to address(0), it will be set to the msgSender + function _setPoolConnectionFor( + ISuperfluidPool pool, + address memberAddr, + bool doConnect, + bytes memory ctx + ) + internal + returns (bool success) { ISuperfluidToken token = pool.superToken(); ISuperfluid.Context memory currentContext = AgreementLibrary.authorizeTokenAccess(token, ctx); - address msgSender = currentContext.msgSender; - newCtx = ctx; - bool isConnected = token.isPoolMemberConnected(this, pool, msgSender); - if (doConnect != isConnected) { - assert( - SuperfluidPool(address(pool)).operatorConnectMember( - msgSender, doConnect, uint32(currentContext.timestamp) - ) - ); + bool autoConnectForOtherMember = false; + if (memberAddr == address(0)) { + memberAddr = currentContext.msgSender; + } else { + autoConnectForOtherMember = true; + } + + bool isConnected = token.isPoolMemberConnected(this, pool, memberAddr); + + if (doConnect != isConnected) { if (doConnect) { - uint32 poolSlotId = - _findAndFillPoolConnectionsBitmap(token, msgSender, bytes32(uint256(uint160(address(pool))))); + if (autoConnectForOtherMember) { + // check if we're below the slot limit for autoconnect + uint256 nUsedSlots = SlotsBitmapLibrary.countUsedSlots( + token, memberAddr, _POOL_SUBS_BITMAP_STATE_SLOT_ID + ); + if (nUsedSlots >= MAX_POOL_AUTO_CONNECT_SLOTS) { + return false; + } + } + + uint32 poolSlotId = _findAndFillPoolConnectionsBitmap( + token, memberAddr, bytes32(uint256(uint160(address(pool)))) + ); token.createPoolConnectivity - (msgSender, GDAv1StorageLib.PoolConnectivity({ slotId: poolSlotId, pool: pool })); + (memberAddr, GDAv1StorageLib.PoolConnectivity({ slotId: poolSlotId, pool: pool })); } else { (, GDAv1StorageLib.PoolConnectivity memory poolConnectivity) = - token.getPoolConnectivity(this, msgSender, pool); - token.deletePoolConnectivity(msgSender, pool); + token.getPoolConnectivity(this, memberAddr, pool); + token.deletePoolConnectivity(memberAddr, pool); - _clearPoolConnectionsBitmap(token, msgSender, poolConnectivity.slotId); + _clearPoolConnectionsBitmap(token, memberAddr, poolConnectivity.slotId); } - emit PoolConnectionUpdated(token, pool, msgSender, doConnect, currentContext.userData); - } - } + assert( + SuperfluidPool(address(pool)).operatorConnectMember( + memberAddr, doConnect, uint32(currentContext.timestamp) + ) + ); - /// @inheritdoc IGeneralDistributionAgreementV1 - function connectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) { - return connectPool(pool, true, ctx); - } + emit PoolConnectionUpdated(token, pool, memberAddr, doConnect, currentContext.userData); + } - /// @inheritdoc IGeneralDistributionAgreementV1 - function disconnectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) { - return connectPool(pool, false, ctx); + return true; } /// @inheritdoc IGeneralDistributionAgreementV1 @@ -533,21 +590,6 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi } } - 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 _adjustBuffer (ISuperfluidToken token, address pool, diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index 6b7c33a895..83234be488 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -123,8 +123,8 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { ISuperfluidToken superToken_, bool transferabilityForUnitsOwner_, bool distributionFromAnyAddress_, - string memory erc20Name_, - string memory erc20Symbol_, + string calldata erc20Name_, + string calldata erc20Symbol_, uint8 erc20Decimals_ ) external initializer { admin = admin_; diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol index 5b7682cbba..a8025e8269 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol @@ -5,14 +5,18 @@ import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.so import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol"; import { SuperfluidPool } from "./SuperfluidPool.sol"; import { PoolConfig, PoolERC20Metadata } from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; +import { ISuperToken } from "../../interfaces/superfluid/ISuperToken.sol"; +import { ISuperfluidPool } from "../../interfaces/agreements/gdav1/ISuperfluidPool.sol"; +import { IPoolAdminNFT } from "../../interfaces/agreements/gdav1/IPoolAdminNFT.sol"; + library SuperfluidPoolDeployerLibrary { function deploy( address beacon, address admin, ISuperfluidToken token, - PoolConfig memory config, - PoolERC20Metadata memory poolERC20Metadata + PoolConfig calldata config, + PoolERC20Metadata calldata poolERC20Metadata ) external returns (SuperfluidPool pool) { bytes memory initializeCallData = abi.encodeWithSelector( SuperfluidPool.initialize.selector, @@ -30,4 +34,25 @@ library SuperfluidPoolDeployerLibrary { ); pool = SuperfluidPool(address(superfluidPoolBeaconProxy)); } + + // This was moved out of GeneralDistributionAgreementV1.sol to reduce the contract size. + function mintPoolAdminNFT(ISuperfluidToken token, ISuperfluidPool pool) external { + 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)); + } + + if (poolAdminNFTAddress != address(0)) { + IPoolAdminNFT(poolAdminNFTAddress).mint(address(pool)); + } + } } diff --git a/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol b/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol index ec91b3e5e4..cbd95cc410 100644 --- a/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol +++ b/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol @@ -1,7 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity >= 0.8.11; -import { ISuperfluid, ISuperToken, ISuperApp, SuperAppDefinitions } from "../interfaces/superfluid/ISuperfluid.sol"; +import { + ISuperfluid, + ISuperToken, + ISuperApp, + SuperAppDefinitions, + IGeneralDistributionAgreementV1 +} from "../interfaces/superfluid/ISuperfluid.sol"; import { SuperTokenV1Library } from "./SuperTokenV1Library.sol"; /** @@ -39,6 +45,16 @@ abstract contract CFASuperAppBase is ISuperApp { */ constructor(ISuperfluid host_) { HOST = host_; + + // disable autoconnect for GDA pools + IGeneralDistributionAgreementV1 gda = IGeneralDistributionAgreementV1( + address( + ISuperfluid(host_).getAgreementClass( + keccak256("org.superfluid-finance.agreements.GeneralDistributionAgreement.v1") + ) + ) + ); + gda.setConnectPermission(false); } /** diff --git a/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol b/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol index 529ec77d57..74f35b1a22 100644 --- a/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol +++ b/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol @@ -47,6 +47,7 @@ import { * `expectRevert` expects a revert in the next call. * If a revert is triggered by library code itself (vs by a call), `expectRevert` will thus not _see_ that. * Possible mitigations: + * - if available, use an overloaded variant which allows to explicitly specify the sender * - avoid higher-level library methods which can themselves trigger reverts in tests where this is an issue * - wrap the method invocation into an external helper method which you then invoke with `this.helperMethod()`, * which makes it an external call @@ -1415,6 +1416,24 @@ library SuperTokenV1Library { return true; } + /** + * @dev Connects a pool member to `pool` (aka "autoconnect") if less than 4 connection slots are occupied. + * @param token The Super Token address. + * @param pool The Superfluid Pool to connect. + * @param memberAddress The address of the member to connect. + * @return success indicates whether the connection was successful. + */ + function tryConnectPoolFor(ISuperToken token, ISuperfluidPool pool, address memberAddress) + internal + returns (bool success) + { + (ISuperfluid host, IGeneralDistributionAgreementV1 gda) = _getAndCacheHostAndGDA(token); + bytes memory ret = host.callAgreement( + gda, abi.encodeCall(gda.tryConnectPoolFor, (pool, memberAddress, new bytes(0))), new bytes(0) + ); + (success, ) = abi.decode(ret, (bool, bytes)); + } + /** * @dev Disconnects a pool member from `pool`. * @param token The Super Token address. @@ -1444,6 +1463,9 @@ library SuperTokenV1Library { * @param pool The Superfluid Pool address. * @param requestedAmount The amount of tokens to distribute. * @return actualAmount The amount actually distributed, which is equal or smaller than `requestedAmount` + * NOTE: in foundry tests, you may unexpectedly get `GDA_DISTRIBUTE_FOR_OTHERS_NOT_ALLOWED` reverts + * because of the use of `address(this)` as the `from` argument. You can work around this by using + * `distribute(token, from, pool, requestedAmount)` instead. */ function distribute(ISuperToken token, ISuperfluidPool pool, uint256 requestedAmount) internal @@ -1499,6 +1521,9 @@ library SuperTokenV1Library { * @param requestedFlowRate The flow rate of tokens to distribute. * @return actualFlowRate The flowrate actually set, which is equal or smaller than `requestedFlowRate`, * depending on pool state - see IGeneralDistributionAgreement.estimateFlowDistributionActualFlowRate(). + * NOTE: in foundry tests, you may unexpectedly get `GDA_DISTRIBUTE_FOR_OTHERS_NOT_ALLOWED` reverts + * because of the use of `address(this)` as the `from` argument. You can work around this by using + * `distributeFlow(token, from, pool, requestedFlowRate)` instead. */ function distributeFlow(ISuperToken token, ISuperfluidPool pool, int96 requestedFlowRate) internal diff --git a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol index d3eb2ea33b..6c7b157eda 100644 --- a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol @@ -37,6 +37,7 @@ abstract contract IGeneralDistributionAgreementV1 is ISuperAgreement { error GDA_NOT_POOL_ADMIN(); // 0x3a87e565 error GDA_NO_ZERO_ADDRESS_ADMIN(); // 0x82c5d837 error GDA_ONLY_SUPER_TOKEN_POOL(); // 0x90028c37 + error GDA_CANNOT_CONNECT_POOL(); // 0x83d98e4c // Events @@ -179,7 +180,7 @@ abstract contract IGeneralDistributionAgreementV1 is ISuperAgreement { /// @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) + function createPool(ISuperfluidToken token, address admin, PoolConfig calldata poolConfig) external virtual returns (ISuperfluidPool pool); @@ -193,7 +194,7 @@ abstract contract IGeneralDistributionAgreementV1 is ISuperAgreement { function createPoolWithCustomERC20Metadata( ISuperfluidToken token, address admin, - PoolConfig memory poolConfig, + PoolConfig calldata poolConfig, PoolERC20Metadata memory poolERC20Metadata ) external virtual returns (ISuperfluidPool pool); @@ -214,6 +215,22 @@ abstract contract IGeneralDistributionAgreementV1 is ISuperAgreement { /// @return newCtx the new context bytes function connectPool(ISuperfluidPool pool, bytes calldata ctx) external virtual returns (bytes memory newCtx); + /// @notice Allows the pool admin to connect a member to the pool if autoconnect slots are available. + /// "autoconnect slots" are a subset of the slots available to pool members themselves. + /// @param pool The pool address + /// @param memberAddr The member address + /// @param ctx Context bytes + /// @return success true if the member was (or remained) connected, false otherwise + /// @return newCtx the new context bytes + function tryConnectPoolFor(ISuperfluidPool pool, address memberAddr, bytes calldata ctx) + external + virtual + returns (bool success, bytes memory newCtx); + + /// @notice Allows accounts to control whether third parties can connect them to pools. By default, they can. + /// @param allow true to allow (only has an effect if it was previously denied), false to deny + function setConnectPermission(bool allow) external virtual; + /// @notice Disconnects `msg.sender` from `pool`. /// @dev This is used to disconnect a pool from the GDA. /// @param pool The pool address diff --git a/packages/ethereum-contracts/contracts/libs/SlotsBitmapLibrary.sol b/packages/ethereum-contracts/contracts/libs/SlotsBitmapLibrary.sol index 3322b7056e..a38e4eea14 100644 --- a/packages/ethereum-contracts/contracts/libs/SlotsBitmapLibrary.sol +++ b/packages/ethereum-contracts/contracts/libs/SlotsBitmapLibrary.sol @@ -12,20 +12,20 @@ import {ISuperfluidToken} from "../interfaces/superfluid/ISuperfluidToken.sol"; * - A data slot can be enabled or disabled with the help of bitmap. * - MAX_NUM_SLOTS is 256 in this implementation (using one uint256) * - Superfluid token storage usage: - * - getAgreementStateSlot(bitmapStateSlotId) stores the bitmap of enabled data slots - * - getAgreementStateSlot(dataStateSlotIDStart + stotId) stores the data of the slot + * - updateAgreementStateSlot(bitmapStateSlotId) stores the bitmap of enabled data slots + * - updateAgreementStateSlot(dataStateSlotIDStart + stotId) stores the data of the slot */ library SlotsBitmapLibrary { uint32 internal constant _MAX_NUM_SLOTS = 256; - function findEmptySlotAndFill( - ISuperfluidToken token, - address account, - uint256 bitmapStateSlotId, - uint256 dataStateSlotIDStart, - bytes32 data - ) + function findEmptySlotAndFill + (ISuperfluidToken token, + address account, + uint256 bitmapStateSlotId, + uint256 dataStateSlotIDStart, + bytes32 data + ) public returns (uint32 slotId) { @@ -55,12 +55,12 @@ library SlotsBitmapLibrary { require(slotId < _MAX_NUM_SLOTS, "SlotBitmap out of bound"); } - function clearSlot( - ISuperfluidToken token, - address account, - uint256 bitmapStateSlotId, - uint32 slotId - ) + function clearSlot + (ISuperfluidToken token, + address account, + uint256 bitmapStateSlotId, + uint32 slotId + ) public { uint256 subsBitmap = uint256(token.getAgreementStateSlot( @@ -78,16 +78,16 @@ library SlotsBitmapLibrary { slotData); } - function listData( - ISuperfluidToken token, - address account, - uint256 bitmapStateSlotId, - uint256 dataStateSlotIDStart - ) + function listData + (ISuperfluidToken token, + address account, + uint256 bitmapStateSlotId, + uint256 dataStateSlotIDStart + ) public view - returns ( - uint32[] memory slotIds, - bytes32[] memory dataList) + returns (uint32[] memory slotIds, + bytes32[] memory dataList + ) { uint256 subsBitmap = uint256(token.getAgreementStateSlot( address(this), @@ -113,4 +113,23 @@ library SlotsBitmapLibrary { mstore(dataList, nSlots) } } + + function countUsedSlots + (ISuperfluidToken token, + address account, + uint256 bitmapStateSlotId + ) + public view + returns (uint256 nUsedSlots) + { + uint256 subsBitmap = uint256(token.getAgreementStateSlot( + address(this), + account, + bitmapStateSlotId, 1)[0]); + + for (uint32 slotId = 0; slotId < _MAX_NUM_SLOTS; ++slotId) { + if ((uint256(subsBitmap >> slotId) & 1) == 0) continue; + ++nUsedSlots; + } + } } diff --git a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol index 163713169e..4714576620 100644 --- a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol +++ b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol @@ -194,6 +194,16 @@ contract SuperfluidFrameworkDeploymentSteps { gdaV1Logic.superfluidPoolBeacon().upgradeTo(address(superfluidPoolLogic)); gdaV1Logic.superfluidPoolBeacon().transferOwnership(address(host)); } + + bytes32 aclPoolConnectExclusiveRoleAdmin = keccak256("ACL_POOL_CONNECT_EXCLUSIVE_ROLE_ADMIN"); + SimpleACL(address(host.getSimpleACL())).setRoleAdmin( + gdaV1.ACL_POOL_CONNECT_EXCLUSIVE_ROLE(), + aclPoolConnectExclusiveRoleAdmin + ); + SimpleACL(address(host.getSimpleACL())).grantRole( + aclPoolConnectExclusiveRoleAdmin, + address(gdaV1) + ); } else if (step == 3) {// PERIPHERAL CONTRACTS: NFT Proxy and Logic { poolAdminNFT = PoolAdminNFT(address(ProxyDeployerLibrary.deployUUPSProxy())); diff --git a/packages/ethereum-contracts/foundry.toml b/packages/ethereum-contracts/foundry.toml index df19e26ee9..cc999464ae 100644 --- a/packages/ethereum-contracts/foundry.toml +++ b/packages/ethereum-contracts/foundry.toml @@ -8,7 +8,7 @@ ignored_error_codes = [ 1699 # assembly { selfdestruct } in contracts/mocks/SuperfluidDestructorMock.sol ] # keep in sync with truffle-config.js -evm_version = 'paris' +evm_version = 'shanghai' optimizer = true optimizer_runs = 200 remappings = [ diff --git a/packages/ethereum-contracts/hardhat.config.ts b/packages/ethereum-contracts/hardhat.config.ts index 309fbdc786..80b4c605b1 100644 --- a/packages/ethereum-contracts/hardhat.config.ts +++ b/packages/ethereum-contracts/hardhat.config.ts @@ -99,7 +99,7 @@ const config: HardhatUserConfig = { enabled: true, runs: 200, }, - evmVersion: "paris", + evmVersion: "shanghai", }, }, paths: { diff --git a/packages/ethereum-contracts/ops-scripts/deploy-framework.js b/packages/ethereum-contracts/ops-scripts/deploy-framework.js index df893c09ec..194ae71af4 100644 --- a/packages/ethereum-contracts/ops-scripts/deploy-framework.js +++ b/packages/ethereum-contracts/ops-scripts/deploy-framework.js @@ -947,6 +947,31 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( if (gdaNewLogicAddress !== ZERO_ADDRESS) { agreementsToUpdate.push(gdaNewLogicAddress); } + + // check/set ACL role admins + const simpleAcl = await SimpleACL.at(simpleAclAddress); + + const aclSuperappRegistrationRoleAdmin = web3.utils.sha3("ACL_SUPERAPP_REGISTRATION_ROLE_ADMIN"); + const aclSuperappRegistrationRole = web3.utils.sha3("ACL_SUPERAPP_REGISTRATION_ROLE"); + if (! await simpleAcl.hasRole(aclSuperappRegistrationRoleAdmin, deployerAddr)) { + await simpleAcl.setRoleAdmin(aclSuperappRegistrationRole, aclSuperappRegistrationRoleAdmin); + console.log("Set ACL_SUPERAPP_REGISTRATION_ROLE admin to ACL_SUPERAPP_REGISTRATION_ROLE_ADMIN"); + await simpleAcl.grantRole(aclSuperappRegistrationRoleAdmin, deployerAddr); + console.log("Granted ACL_SUPERAPP_REGISTRATION_ROLE_ADMIN to deployerAddr"); + } else { + console.log("ACL_SUPERAPP_REGISTRATION_ROLE_ADMIN already granted to deployerAddr"); + } + + const aclPoolConnectExclusiveRole = web3.utils.sha3("ACL_POOL_CONNECT_EXCLUSIVE_ROLE"); + const aclPoolConnectExclusiveRoleAdmin = web3.utils.sha3("ACL_POOL_CONNECT_EXCLUSIVE_ROLE_ADMIN"); + if (! await simpleAcl.hasRole(aclPoolConnectExclusiveRoleAdmin, gdaProxyAddr)) { + await simpleAcl.setRoleAdmin(aclPoolConnectExclusiveRole, aclPoolConnectExclusiveRoleAdmin); + console.log("Set ACL_POOL_CONNECT_EXCLUSIVE_ROLE admin to ACL_POOL_CONNECT_EXCLUSIVE_ROLE_ADMIN"); + await simpleAcl.grantRole(aclPoolConnectExclusiveRoleAdmin, gdaProxyAddr); + console.log("Granted ACL_POOL_CONNECT_EXCLUSIVE_ROLE to GDA"); + } else { + console.log("ACL_POOL_CONNECT_EXCLUSIVE_ROLE_ADMIN already granted to GDA"); + } } // deploy new super token factory logic (depends on SuperToken logic, which links to nft deployer library) diff --git a/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol index 2af3bc4e5a..e680cd1748 100644 --- a/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol +++ b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol @@ -87,7 +87,7 @@ contract FoundrySuperfluidTester is Test { } struct ExpectedPoolMemberData { - bool isConnected; + bool wasConnected; uint128 ownedUnits; int96 flowRate; int96 netFlowRate; @@ -128,6 +128,8 @@ contract FoundrySuperfluidTester is Test { address internal constant ivan = address(0x429); address[] internal TEST_ACCOUNTS = [admin, alice, bob, carol, dan, eve, frank, grace, heidi, ivan]; + address internal constant MAX_TESTER_ADDRESS = address(0x4ff); + /// @dev Other account addresses added that aren't testers (pools, super apps, smart contracts) EnumerableSet.AddressSet internal OTHER_ACCOUNTS; @@ -1204,7 +1206,7 @@ contract FoundrySuperfluidTester is Test { vm.assume(newUnits_ < type(uint72).max); ISuperToken poolSuperToken = ISuperToken(address(pool_.superToken())); - (bool isConnected, int256 oldUnits,) = _helperGetMemberPoolState(pool_, member_); + (bool wasConnected, int256 oldUnits,) = _helperGetMemberPoolState(pool_, member_); PoolUnitData memory poolUnitDataBefore = _helperGetPoolUnitsData(pool_); @@ -1220,8 +1222,11 @@ contract FoundrySuperfluidTester is Test { assertEq(pool_.getUnits(member_), newUnits_, "GDAv1.t: Members' units incorrectly set"); - // Assert that pending balance didn't change if user is disconnected - if (!isConnected) { + // Determine the new connection status after the update + bool isConnectedAfter = sf.gda.isMemberConnected(pool_, member_); + + // Assert that pending balance didn't change if user was and remains disconnected + if (!wasConnected && !isConnectedAfter) { (int256 balanceAfter,,,) = poolSuperToken.realtimeBalanceOfNow(member_); assertEq( balanceAfter, balanceBefore, "_helperUpdateMemberUnits: Pending balance changed" @@ -1253,15 +1258,47 @@ contract FoundrySuperfluidTester is Test { poolUnitDataAfter.totalUnits, "_helperUpdateMemberUnits: Pool total units incorrect" ); + + // Calculate expected connected units change based on new behavior + int256 expectedConnectedUnitsDelta; + if (wasConnected && isConnectedAfter) { + // Member was connected and remains connected - units delta applies to connected + expectedConnectedUnitsDelta = unitsDelta; + } else if (!wasConnected && isConnectedAfter) { + // Member was disconnected and is now connected - all new units go to connected + expectedConnectedUnitsDelta = uint256(newUnits_).toInt256(); + } else if (wasConnected && !isConnectedAfter) { + // Member was connected and is now disconnected - all old units move to disconnected + expectedConnectedUnitsDelta = -oldUnits; + } else { + // Member was disconnected and remains disconnected - units delta applies to disconnected + expectedConnectedUnitsDelta = 0; + } + assertEq( - uint256(uint256(poolUnitDataBefore.connectedUnits).toInt256() + (isConnected ? unitsDelta : int128(0))), + uint256(uint256(poolUnitDataBefore.connectedUnits).toInt256() + expectedConnectedUnitsDelta), poolUnitDataAfter.connectedUnits, "_helperUpdateMemberUnits: Pool connected units incorrect" ); + + // Calculate expected disconnected units change based on new behavior + int256 expectedDisconnectedUnitsDelta; + if (wasConnected && isConnectedAfter) { + // Member was connected and remains connected - no change to disconnected + expectedDisconnectedUnitsDelta = 0; + } else if (!wasConnected && isConnectedAfter) { + // Member was disconnected and is now connected - all old units move from disconnected to connected + expectedDisconnectedUnitsDelta = -oldUnits; + } else if (wasConnected && !isConnectedAfter) { + // Member was connected and is now disconnected - all new units go to disconnected + expectedDisconnectedUnitsDelta = uint256(newUnits_).toInt256(); + } else { + // Member was disconnected and remains disconnected - units delta applies to disconnected + expectedDisconnectedUnitsDelta = unitsDelta; + } + assertEq( - uint256( - uint256(poolUnitDataBefore.disconnectedUnits).toInt256() + (isConnected ? int128(0) : unitsDelta) - ), + uint256(uint256(poolUnitDataBefore.disconnectedUnits).toInt256() + expectedDisconnectedUnitsDelta), poolUnitDataAfter.disconnectedUnits, "_helperUpdateMemberUnits: Pool disconnected units incorrect" ); @@ -1289,20 +1326,38 @@ contract FoundrySuperfluidTester is Test { function _helperConnectPool(address caller_, ISuperToken superToken_, ISuperfluidPool pool_, bool useForwarder_) internal { - (bool isConnectedBefore, int256 oldUnits, int96 oldFlowRate) = _helperGetMemberPoolState(pool_, caller_); + _helperConnectPoolFor(caller_, caller_, superToken_, pool_, useForwarder_); + } + + function _helperConnectPoolFor(address member_, address caller_, ISuperToken superToken_, ISuperfluidPool pool_, bool useForwarder_) + internal + { + (bool isConnectedBefore, int256 oldUnits, int96 oldFlowRate) = _helperGetMemberPoolState(pool_, member_); PoolUnitData memory poolUnitDataBefore = _helperGetPoolUnitsData(pool_); PoolFlowRateData memory poolFlowRateDataBefore = _helperGetPoolFlowRatesData(pool_); vm.startPrank(caller_); if (useForwarder_) { - sf.gdaV1Forwarder.connectPool(pool_, ""); + if (caller_ == member_) { + sf.gdaV1Forwarder.connectPool(pool_, ""); + } else { + revert("autoconnect not supported by forwarder"); + } } else { - sf.host.callAgreement( - sf.gda, - abi.encodeWithSelector(IGeneralDistributionAgreementV1.connectPool.selector, pool_, ""), - new bytes(0) - ); + if (caller_ == member_) { + sf.host.callAgreement( + sf.gda, + abi.encodeWithSelector(IGeneralDistributionAgreementV1.connectPool.selector, pool_, ""), + new bytes(0) + ); + } else { + sf.host.callAgreement( + sf.gda, + abi.encodeWithSelector(IGeneralDistributionAgreementV1.tryConnectPoolFor.selector, pool_, member_, ""), + new bytes(0) + ); + } } vm.stopPrank(); @@ -1310,12 +1365,12 @@ contract FoundrySuperfluidTester is Test { PoolFlowRateData memory poolFlowRateDataAfter = _helperGetPoolFlowRatesData(pool_); { - _helperTakeBalanceSnapshot(superToken_, caller_); + _helperTakeBalanceSnapshot(superToken_, member_); } bool isMemberConnected = useForwarder_ - ? sf.gdaV1Forwarder.isMemberConnected(pool_, caller_) - : sf.gda.isMemberConnected(pool_, caller_); + ? sf.gdaV1Forwarder.isMemberConnected(pool_, member_) + : sf.gda.isMemberConnected(pool_, member_); assertEq(isMemberConnected, true, "GDAv1.t: Member not connected"); // Assert connected units delta for the pool @@ -1690,10 +1745,10 @@ contract FoundrySuperfluidTester is Test { function _helperGetMemberPoolState(ISuperfluidPool pool_, address member_) internal view - returns (bool isConnected, int256 units, int96 flowRate) + returns (bool wasConnected, int256 units, int96 flowRate) { units = uint256(pool_.getUnits(member_)).toInt256(); - isConnected = sf.gda.isMemberConnected(pool_, member_); + wasConnected = sf.gda.isMemberConnected(pool_, member_); flowRate = pool_.getMemberFlowRate(member_); } diff --git a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol index 1512a48d2f..0c3050b781 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.23; import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; import "@superfluid-finance/solidity-semantic-money/src/SemanticMoney.sol"; import "../../FoundrySuperfluidTester.t.sol"; import { @@ -949,6 +950,7 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste ) public { vm.assume(member != address(0)); vm.assume(member != address(freePool)); + vm.assume(member != alice); // alice is the test distributor vm.assume(units > 0); vm.assume(distributionAmount > 0); vm.assume(units < distributionAmount); @@ -971,8 +973,10 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste (int256 balanceAfter1,,,) = superToken.realtimeBalanceOfNow(member); (int256 claimableAfter1,) = pool.getClaimableNow(member); - assertEq(balanceAfter1, balanceBefore, "Disconnected member balance should not change"); - assertTrue(claimableAfter1 > claimableBefore, "Disconnected member claimable amount should increase"); + if (!sf.gda.isMemberConnected(pool, member)) { + assertEq(balanceAfter1, balanceBefore, "Disconnected member balance should not change"); + assertTrue(claimableAfter1 > claimableBefore, "Disconnected member claimable amount should increase"); + } // Step 2: Connect member and distribute again _helperConnectPool(member, superToken, pool, useForwarder); @@ -986,14 +990,119 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste assertEq(claimableAfter2, 0, "Connected member claimable amount should be 0"); } + function testAutoConnect(address member, uint128 units, uint64 distributionAmount) public { + vm.assume(member != address(0)); + vm.assume(member != address(freePool)); + vm.assume(member != alice); // alice is the test distributor + vm.assume(units > 0); + vm.assume(units < distributionAmount); + + uint256 expectedAmount = (distributionAmount / units) * units; + + uint256 balanceBefore = superToken.balanceOf(member); + + vm.startPrank(alice); + // update units to non-zero and connect the pool + freePool.updateMemberUnits(member, units); + sf.host.callAgreement( + sf.gda, + abi.encodeCall(sf.gda.tryConnectPoolFor, (freePool, member, new bytes(0))), + new bytes(0) + ); + assertEq(freePool.getUnits(member), units); + assertEq(sf.gda.isMemberConnected(freePool, member), true, "member should be (auto)connected"); + + // distribute tokens: this is supposed to show up as balance, with claimable amount remaining 0 + superToken.distribute(alice, freePool, distributionAmount); + + assertEq(superToken.balanceOf(member), balanceBefore + expectedAmount, "balance != distributionAmount"); + assertEq(freePool.getClaimable(member, uint32(block.timestamp)), 0, "claimable != 0"); + vm.stopPrank(); + } + + function testAutoConnectSlotLimit() public { + for (uint256 i = 0; i < sf.gda.MAX_POOL_AUTO_CONNECT_SLOTS() * 2; ++i) { + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, PoolConfig({ transferabilityForUnitsOwner: false, distributionFromAnyAddress: true })); + vm.startPrank(alice); + // update units to non-zero and connect the pool + pool.updateMemberUnits(bob, 1); + + bytes memory ret = sf.host.callAgreement( + sf.gda, + abi.encodeCall(sf.gda.tryConnectPoolFor, (pool, bob, new bytes(0))), + new bytes(0) + ); + (bool success, ) = abi.decode(ret, (bool, bytes)); + if (i < sf.gda.MAX_POOL_AUTO_CONNECT_SLOTS()) { + assertEq(success, true, "success != true"); + assertEq(sf.gda.isMemberConnected(pool, bob), true, "bob should be (auto)connected"); + } else { + assertEq(success, false, "success != false"); + assertEq(sf.gda.isMemberConnected(pool, bob), false, "bob should not be (auto)connected"); + } + vm.stopPrank(); + } + } + + function testAutoConnectPermissioning() public { + vm.startPrank(bob); + sf.gda.setConnectPermission(false); + vm.stopPrank(); + + vm.startPrank(alice); + freePool.updateMemberUnits(bob, 1); + sf.host.callAgreement( + sf.gda, + abi.encodeCall(sf.gda.tryConnectPoolFor, (freePool, bob, new bytes(0))), + new bytes(0) + ); + assertEq(sf.gda.isMemberConnected(freePool, bob), false, "member should not be (auto)connected"); + + // alice can't revoke the opt-out + IAccessControl simpleACL = sf.host.getSimpleACL(); + bytes32 aclRole = sf.gda.ACL_POOL_CONNECT_EXCLUSIVE_ROLE(); //need this ext call before the expectRevert + vm.expectRevert(); + simpleACL.revokeRole(aclRole, bob); + vm.stopPrank(); + + // bob changes his mind and gives permission + + vm.startPrank(bob); + sf.gda.setConnectPermission(true); + vm.stopPrank(); + + // now alice can connect bob + vm.startPrank(alice); + freePool.updateMemberUnits(bob, 1); + sf.host.callAgreement( + sf.gda, + abi.encodeCall(sf.gda.tryConnectPoolFor, (freePool, bob, new bytes(0))), + new bytes(0) + ); + assertEq(sf.gda.isMemberConnected(freePool, bob), true, "member should not be (auto)connected"); + vm.stopPrank(); + + + // cannot connect a pool + ISuperfluidPool anotherPool = _helperCreatePool(superToken, alice, alice, false, PoolConfig({ transferabilityForUnitsOwner: false, distributionFromAnyAddress: true })); + vm.startPrank(alice); + vm.expectRevert(IGeneralDistributionAgreementV1.GDA_CANNOT_CONNECT_POOL.selector); + sf.host.callAgreement( + sf.gda, + abi.encodeCall(sf.gda.tryConnectPoolFor, (freePool, address(anotherPool), new bytes(0))), + new bytes(0) + ); + vm.stopPrank(); + } + /*////////////////////////////////////////////////////////////////////////// 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 + uint8 a; // action types: 0 update units, 1 distribute flow, 2 freePool claim for, 3 freePool connection, + // 4 distribute, 5: freePool autoconnect uint32 v; // action param uint16 dt; // time delta } @@ -1005,7 +1114,7 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste emit log_named_string("", ""); emit log_named_uint(">>> STEP", i); PoolUpdateStep memory s = steps[i]; - uint256 action = s.a % 5; + uint256 action = s.a % 6; uint256 u = 1 + s.u % N_MEMBERS; address user = TEST_ACCOUNTS[u]; @@ -1041,6 +1150,11 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste emit log_named_string("action", "distribute"); emit log_named_uint("distributionAmount", s.v); _helperDistributeViaGDA(superToken, user, user, freePool, uint256(s.v), useBools_.useForwarder); + } else if (action == 5) { + address u4 = TEST_ACCOUNTS[1 + (s.v % N_MEMBERS)]; + emit log_named_string("action", "tryConnectPoolFor"); + emit log_named_address("connect for", u4); + _helperConnectPoolFor(user, u4, superToken, freePool, false); } else { assert(false); } diff --git a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreementV1.prop.t.sol b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreementV1.prop.t.sol index 0b1956df68..cfef124724 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreementV1.prop.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreementV1.prop.t.sol @@ -14,7 +14,7 @@ import { ISuperAgreement } from "../../../../contracts/interfaces/superfluid/ISu import { GeneralDistributionAgreementV1, PoolConfig, - ISuperfluid, ISuperfluidPool, ISuperToken + ISuperfluid, ISuperfluidPool } from "../../../../contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol"; import { GDAv1StorageLib, GDAv1StorageReader, GDAv1StorageWriter diff --git a/packages/ethereum-contracts/test/foundry/apps/CrossStreamSuperApp.t.sol b/packages/ethereum-contracts/test/foundry/apps/CrossStreamSuperApp.t.sol index 3f119fbe6b..7f7cb660d1 100644 --- a/packages/ethereum-contracts/test/foundry/apps/CrossStreamSuperApp.t.sol +++ b/packages/ethereum-contracts/test/foundry/apps/CrossStreamSuperApp.t.sol @@ -64,7 +64,7 @@ contract CrossStreamSuperAppTest is FoundrySuperfluidTester { _addAccount(address(superApp)); } - function testNoTokensMintedOrBurnedInCrossStreamSuperApp(int96 flowRate, uint32 blockTimestamp) public { + function testNoTokensMintedOrBurnedInCrossStreamSuperApp(int96 flowRate, uint24 blockTimestamp) public { // @note due to clipping, there is precision loss, therefore if the flow rate is too low // tokens will be unrecoverable flowRate = int96(bound(flowRate, 2 ** 31 - 1, 1e14)); diff --git a/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol b/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol index a7560519e3..8983b1a162 100644 --- a/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol +++ b/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol @@ -362,7 +362,7 @@ contract SuperTokenV1LibraryTest is FoundrySuperfluidTester { ISuperfluidPool pool = superToken.createPool( address(this), PoolConfig({ - transferabilityForUnitsOwner: false, + transferabilityForUnitsOwner: false, distributionFromAnyAddress: true }) ); @@ -448,6 +448,25 @@ contract SuperTokenV1LibraryTest is FoundrySuperfluidTester { assertFalse(sf.host.isAppJailed(ISuperApp(superAppAddr)), "superApp is jailed"); } + function testTryConnectPoolFor() external { + for (uint256 i = 0; i < sf.gda.MAX_POOL_AUTO_CONNECT_SLOTS() * 2; ++i) { + ISuperfluidPool pool = superToken.createPool(); + pool.updateMemberUnits(bob, 1); + + vm.startPrank(alice); + bool success = superToken.tryConnectPoolFor(pool, bob); + vm.stopPrank(); + + if (i < sf.gda.MAX_POOL_AUTO_CONNECT_SLOTS()) { + assertEq(success, true, "success != true"); + assertEq(sf.gda.isMemberConnected(pool, bob), true, "bob should be (auto)connected"); + } else { + assertEq(success, false, "success != false"); + assertEq(sf.gda.isMemberConnected(pool, bob), false, "bob should not be (auto)connected"); + } + } + } + // HELPER FUNCTIONS ======================================================================================== // direct use of the agreement for assertions diff --git a/packages/ethereum-contracts/test/foundry/libs/SlotsBitmapLibrary.prop.t.sol b/packages/ethereum-contracts/test/foundry/libs/SlotsBitmapLibrary.prop.t.sol index abe99982f8..cbe46abaee 100644 --- a/packages/ethereum-contracts/test/foundry/libs/SlotsBitmapLibrary.prop.t.sol +++ b/packages/ethereum-contracts/test/foundry/libs/SlotsBitmapLibrary.prop.t.sol @@ -46,6 +46,11 @@ contract SlotsBitmapLibraryPropertyTest is Test { (slotIds, dataList) = SlotsBitmapLibrary.listData( _superToken, _subscriber, _SUBSCRIBER_SUBS_BITMAP_STATE_SLOT_ID, _SUBSCRIBER_SUB_DATA_STATE_SLOT_ID_START ); + uint256 n = SlotsBitmapLibrary.countUsedSlots( + _superToken, _subscriber, _SUBSCRIBER_SUBS_BITMAP_STATE_SLOT_ID + ); + assertEq(slotIds.length, n, "slotsIds.length"); + assertEq(dataList.length, n, "dataList.length"); } /** diff --git a/packages/ethereum-contracts/truffle-config.js b/packages/ethereum-contracts/truffle-config.js index 15a49f4727..aaadfb4f1b 100644 --- a/packages/ethereum-contracts/truffle-config.js +++ b/packages/ethereum-contracts/truffle-config.js @@ -389,9 +389,7 @@ const E = (module.exports = { runs: 200, }, // see https://docs.soliditylang.org/en/latest/using-the-compiler.html#target-options - // we don't switch to "shanghai" or later as long as there's networks - // without EIP-3855 support (PUSH0) - evmVersion: "paris", + evmVersion: "shanghai", }, }, },