From ba1b0674aba6c0297d826ca326b57678d3db4901 Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 26 Jun 2025 16:34:40 +0200 Subject: [PATCH 01/52] distinguish between unsettled and claimable balance in pools --- .../gdav1/GeneralDistributionAgreementV1.sol | 2 +- .../agreements/gdav1/SuperfluidPool.sol | 31 +++++++++---- .../gdav1/GeneralDistributionAgreement.t.sol | 46 +++++++++++++++++++ 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index c7c02135ff..a7a7ed69a7 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -142,7 +142,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi _getPoolMemberData(token, account, ISuperfluidPool(pool)); assert(exist); assert(poolMemberData.pool == pool); - fromPools += ISuperfluidPool(pool).getClaimable(account, uint32(time)); + fromPools += SuperfluidPool(pool).getUnsettledValue(account, uint32(time)); } } rtb += fromPools; diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index 60d586a76c..4bbb820f4f 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -374,12 +374,9 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { /// @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) - ); + return GDA.isMemberConnected(ISuperfluidPool(address(this)), memberAddr) + ? int256(0) + : getUnsettledValue(memberAddr, time); } /// @inheritdoc ISuperfluidPool @@ -478,10 +475,23 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { _handlePoolMemberNFT(memberAddr, newUnits); } - function _claimAll(address memberAddr, uint32 time) internal returns (int256 amount) { - amount = getClaimable(memberAddr, time); + function _settle(address memberAddr, int256 amount) internal { assert(GDA.poolSettleClaim(superToken, memberAddr, (amount))); _membersData[memberAddr].claimedValue += amount; + } + + function getUnsettledValue(address memberAddr, uint32 time) public view 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) + ); + } + + function _claimAll(address memberAddr, uint32 time) internal returns (int256 amount) { + amount = getClaimable(memberAddr, time); + _settle(memberAddr, amount); emit DistributionClaimed(superToken, memberAddr, amount, _membersData[memberAddr].claimedValue); } @@ -511,10 +521,11 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { // 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); + int256 settleAmount = getUnsettledValue(memberAddr, time); + _settle(memberAddr, settleAmount); int128 units = uint256(_getUnits(memberAddr)).toInt256().toInt128(); if (doConnect) { - _shiftDisconnectedUnits(Unit.wrap(-units), Value.wrap(claimedAmount), Time.wrap(time)); + _shiftDisconnectedUnits(Unit.wrap(-units), Value.wrap(settleAmount), Time.wrap(time)); } else { _shiftDisconnectedUnits(Unit.wrap(units), Value.wrap(0), Time.wrap(time)); } 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 f7bc8156e7..8b453103a0 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -906,6 +906,52 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste _helperSuperfluidPoolUnitsTransferFrom(freePool, spender, owner, spender, uint256(uint128(transferAmount))); } + function testGetClaimable( + address member, + uint128 units, + uint64 distributionAmount, + bool useForwarder, + PoolConfig memory config + ) public { + vm.assume(member != address(0)); + vm.assume(member != address(freePool)); + vm.assume(units > 0); + vm.assume(distributionAmount > 0); + vm.assume(units < distributionAmount); + vm.assume(distributionAmount < type(uint128).max); + + // Create a pool for testing + ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, useForwarder, config); + _addAccount(member); + + // Step 1: Assign units to disconnected member + _helperUpdateMemberUnits(pool, alice, member, units, _StackVars_UseBools({useForwarder: useForwarder, useGDA: false})); + + (int256 balanceBefore,,,) = superToken.realtimeBalanceOfNow(member); + (int256 claimableBefore,) = pool.getClaimableNow(member); + + // Distribute + _helperDistributeViaGDA(superToken, alice, alice, pool, distributionAmount, useForwarder); + + // Check disconnected member: expect balance unchanged, claimable increased + (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"); + + // Step 2: Connect member and distribute again + _helperConnectPool(member, superToken, pool, useForwarder); + _helperDistributeViaGDA(superToken, alice, alice, pool, distributionAmount, useForwarder); + + (int256 balanceAfter2,,,) = superToken.realtimeBalanceOfNow(member); + (int256 claimableAfter2,) = pool.getClaimableNow(member); + + // Check connected member: balance increased, claimable remains 0 + assertTrue(balanceAfter2 > balanceAfter1, "Connected member balance should increase"); + assertEq(claimableAfter2, 0, "Connected member claimable amount should be 0"); + } + /*////////////////////////////////////////////////////////////////////////// Assertion Functions //////////////////////////////////////////////////////////////////////////*/ From f76ce16b6df8d9415002a9301130aa585da82baf Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 26 Jun 2025 16:43:40 +0200 Subject: [PATCH 02/52] add assertions --- .../test/foundry/FoundrySuperfluidTester.t.sol | 15 +++++++++++++++ .../gdav1/GeneralDistributionAgreement.t.sol | 4 +--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol index b289cace21..7ad994aee7 100644 --- a/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol +++ b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol @@ -1278,6 +1278,21 @@ contract FoundrySuperfluidTester is Test { // _assertRealTimeBalances(ISuperToken(address(poolSuperToken))); } + function _helperClaimAll(ISuperfluidPool pool_, address caller_, address member_) internal { + (int256 claimableBefore,) = pool_.getClaimableNow(member_); + (int256 balanceBefore,,,) = pool_.superToken().realtimeBalanceOfNow(member_); + + vm.startPrank(caller_); + pool_.claimAll(member_); + vm.stopPrank(); + + (int256 claimableAfter,) = pool_.getClaimableNow(member_); + (int256 balanceAfter,,,) = pool_.superToken().realtimeBalanceOfNow(member_); + + assertEq(claimableAfter, 0, "GDAv1.t: Member claimable amount should be 0"); + assertEq(balanceAfter, balanceBefore + claimableBefore, "GDAv1.t: Member balance should increase by claimable amount"); + } + function _helperConnectPool(address caller_, ISuperToken superToken_, ISuperfluidPool pool_, bool useForwarder_) internal { 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 8b453103a0..5415d34182 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -995,9 +995,7 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste 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(); + _helperClaimAll(freePool, user, u4); } else if (action == 3) { bool doConnect = s.v % 2 == 0 ? false : true; emit log_named_string("action", "doConnectPool"); From 9c433c503f23328b393ec553c20bdfc5d5f87c3d Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 15 Jul 2025 18:12:29 +0200 Subject: [PATCH 03/52] use new reader function --- .../contracts/agreements/gdav1/SuperfluidPool.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index 612d353594..472bcd2160 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -375,7 +375,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { /// @inheritdoc ISuperfluidPool function getClaimable(address memberAddr, uint32 time) public view override returns (int256) { - return GDA.isMemberConnected(ISuperfluidPool(address(this)), memberAddr) + return superToken.isPoolMemberConnected(GDA, ISuperfluidPool(address(this)), memberAddr) ? int256(0) : getUnsettledValue(memberAddr, time); } From d6687abec81bfa8f19e6ebd3c55b7a6fb59b4d42 Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 16 Jul 2025 17:19:44 +0200 Subject: [PATCH 04/52] small fix --- packages/ethereum-contracts/ops-scripts/deploy-framework.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ethereum-contracts/ops-scripts/deploy-framework.js b/packages/ethereum-contracts/ops-scripts/deploy-framework.js index f53b38a014..df893c09ec 100644 --- a/packages/ethereum-contracts/ops-scripts/deploy-framework.js +++ b/packages/ethereum-contracts/ops-scripts/deploy-framework.js @@ -996,7 +996,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( const poolMemberNFTPAddr = await superTokenLogic.POOL_MEMBER_NFT(); let poolMemberNFTLAddr = ZERO_ADDRESS; if (poolMemberNFTPAddr !== ZERO_ADDRESS) { - const poolMemberNFTContract = await PoolMemberNFT.at(poolMemberNFTPAddr); + const poolMemberNFTContract = await UUPSProxiable.at(poolMemberNFTPAddr); poolMemberNFTLAddr = await poolMemberNFTContract.getCodeAddress(); } From 5dad0c3565b9bb9a334ee0cf021bb089b012e802 Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 16 Jul 2025 17:41:57 +0200 Subject: [PATCH 05/52] updated changelog --- packages/ethereum-contracts/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index 0905546b6a..7747002294 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -11,6 +11,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `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. +### 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). + ### Breaking - PoolMemberNFT pruning: `IPoolMemberNFT` and `PoolMemberNFT` removed, `POOL_MEMBER_NFT()` removed from `ISuperToken`. From 882f654f56b65e3381167de4411a1c9e48d558b1 Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Thu, 17 Jul 2025 11:30:39 +0300 Subject: [PATCH 06/52] limit valid input size for _settle() --- .../agreements/gdav1/SuperfluidPool.sol | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index 472bcd2160..5415c4f85e 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -289,7 +289,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { Value.unwrap( // PDPoolMemberMU(poolIndex, memberData) PDPoolMemberMU(poolIndexDataToPDPoolIndex(_index), _memberDataToPDPoolMember(memberData)).settle( - Time.wrap(uint32(block.timestamp)) + Time.wrap(SafeCast.toUint32(block.timestamp)) ).m._settled_value ) ); @@ -370,7 +370,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { returns (int256 claimableBalance, uint256 timestamp) { timestamp = ISuperfluid(superToken.getHost()).getNow(); - return (getClaimable(memberAddr, uint32(timestamp)), timestamp); + return (getClaimable(memberAddr, SafeCast.toUint32(timestamp)), timestamp); } /// @inheritdoc ISuperfluidPool @@ -427,7 +427,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { if (superToken.isPool(GDA, memberAddr)) revert SUPERFLUID_POOL_NO_POOL_MEMBERS(); if (memberAddr == address(0)) revert SUPERFLUID_POOL_NO_ZERO_ADDRESS(); - uint32 time = uint32(ISuperfluid(superToken.getHost()).getNow()); + uint32 time = SafeCast.toUint32(ISuperfluid(superToken.getHost()).getNow()); Time t = Time.wrap(time); Unit wrappedUnits = toSemanticMoneyUnit(newUnits); @@ -456,8 +456,9 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { emit MemberUnitsUpdated(superToken, memberAddr, oldUnits, newUnits); } - function _settle(address memberAddr, int256 amount) internal { - assert(GDA.poolSettleClaim(superToken, memberAddr, (amount))); + function _settle(address memberAddr, uint32 time) internal returns (int256 amount) { + amount = getUnsettledValue(memberAddr, time); + assert(GDA.poolSettleClaim(superToken, memberAddr, amount)); _membersData[memberAddr].claimedValue += amount; } @@ -471,9 +472,10 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { } function _claimAll(address memberAddr, uint32 time) internal returns (int256 amount) { - amount = getClaimable(memberAddr, time); - _settle(memberAddr, amount); - + // For connected pool, claimable amount is zero; hence, we skip. + if (!superToken.isPoolMemberConnected(GDA, ISuperfluidPool(address(this)), memberAddr)) { + amount = _settle(memberAddr, time); + } emit DistributionClaimed(superToken, memberAddr, amount, _membersData[memberAddr].claimedValue); } @@ -485,7 +487,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { /// @inheritdoc ISuperfluidPool function claimAll(address memberAddr) public returns (bool) { bool isConnected = superToken.isPoolMemberConnected(GDA, this, memberAddr); - uint32 time = uint32(ISuperfluid(superToken.getHost()).getNow()); + uint32 time = SafeCast.toUint32(ISuperfluid(superToken.getHost()).getNow()); int256 claimedAmount = _claimAll(memberAddr, time); if (!isConnected) { _shiftDisconnectedUnits(Unit.wrap(0), Value.wrap(claimedAmount), Time.wrap(time)); @@ -502,8 +504,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { // 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 settleAmount = getUnsettledValue(memberAddr, time); - _settle(memberAddr, settleAmount); + int256 settleAmount = _settle(memberAddr, time); int128 units = uint256(_getUnits(memberAddr)).toInt256().toInt128(); if (doConnect) { _shiftDisconnectedUnits(Unit.wrap(-units), Value.wrap(settleAmount), Time.wrap(time)); From e27cb4181602fa9df8414d267d99c44484f8bdfe Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Thu, 17 Jul 2025 11:33:54 +0300 Subject: [PATCH 07/52] remove some silly type convrersions --- .../contracts/agreements/gdav1/SuperfluidPool.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index 5415c4f85e..6b7c33a895 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -375,7 +375,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { /// @inheritdoc ISuperfluidPool function getClaimable(address memberAddr, uint32 time) public view override returns (int256) { - return superToken.isPoolMemberConnected(GDA, ISuperfluidPool(address(this)), memberAddr) + return superToken.isPoolMemberConnected(GDA, this, memberAddr) ? int256(0) : getUnsettledValue(memberAddr, time); } @@ -440,7 +440,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { PDPoolMemberMU memory mu = PDPoolMemberMU(pdPoolIndex, pdPoolMember); // update pool's disconnected units - if (!superToken.isPoolMemberConnected(GDA, ISuperfluidPool(address(this)), memberAddr)) { + if (!superToken.isPoolMemberConnected(GDA, this, memberAddr)) { _shiftDisconnectedUnits(wrappedUnits - mu.m.owned_units, Value.wrap(0), t); } @@ -473,7 +473,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { function _claimAll(address memberAddr, uint32 time) internal returns (int256 amount) { // For connected pool, claimable amount is zero; hence, we skip. - if (!superToken.isPoolMemberConnected(GDA, ISuperfluidPool(address(this)), memberAddr)) { + if (!superToken.isPoolMemberConnected(GDA, this, memberAddr)) { amount = _settle(memberAddr, time); } emit DistributionClaimed(superToken, memberAddr, amount, _membersData[memberAddr].claimedValue); From 5a0fb64cbe9d038a8f401d993f544c0fb1ebebeb Mon Sep 17 00:00:00 2001 From: pavedroad <138004431+pavedroad@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:46:52 +0800 Subject: [PATCH 08/52] chore: remove redundant words in comment (#2092) Signed-off-by: pavedroad --- .../scheduler/contracts/VestingSchedulerV2.sol | 2 +- .../scheduler/contracts/VestingSchedulerV3.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol index dce469187b..89464102a6 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol @@ -441,7 +441,7 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { if (!disableClaimCheck && schedule.claimValidityDate != 0) revert ScheduleNotClaimed(); - // Ensure that that the claming date is after the cliff/flow date and before the claim validity date + // Ensure that the claming date is after the cliff/flow date and before the claim validity date if (schedule.cliffAndFlowDate > block.timestamp || _lteDateToExecuteCliffAndFlow(schedule) < block.timestamp) revert TimeWindowInvalid(); diff --git a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV3.sol b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV3.sol index 4170be4fc2..d8445d1478 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV3.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV3.sol @@ -760,7 +760,7 @@ contract VestingSchedulerV3 is IVestingSchedulerV3, IRelayRecipient { revert ScheduleNotClaimed(); } - // Ensure that that the claming date is after the cliff/flow date and before the claim validity date + // Ensure that the claming date is after the cliff/flow date and before the claim validity date if (schedule.cliffAndFlowDate > block.timestamp || _lteDateToExecuteCliffAndFlow(schedule) < block.timestamp) { revert TimeWindowInvalid(); } From b856b8afde71ff7ba6f339eb1ff8c1b641ad186f Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 17 Jul 2025 10:47:08 +0200 Subject: [PATCH 09/52] added GDA function which allows the pool admin to connect pools on behalf of members --- lib/forge-std | 2 +- .../gdav1/GeneralDistributionAgreementV1.sol | 117 +++++++++++++----- .../gdav1/IGeneralDistributionAgreementV1.sol | 11 ++ packages/ethereum-contracts/foundry.toml | 2 +- .../foundry/FoundrySuperfluidTester.t.sol | 55 ++++++-- .../gdav1/GeneralDistributionAgreement.t.sol | 66 +++++++++- 6 files changed, 210 insertions(+), 43 deletions(-) diff --git a/lib/forge-std b/lib/forge-std index 07263d193d..60acb7aaad 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 07263d193d621c4b2b0ce8b4d54af58f6957d97d +Subproject commit 60acb7aaadcce2d68e52986a0a66fe79f07d138f diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index 2ecb3e7671..195c61b9b9 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -48,6 +48,9 @@ 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 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 @@ -305,49 +308,86 @@ 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) { + ISuperfluidToken token = pool.superToken(); + ISuperfluid.Context memory currentContext = AgreementLibrary.authorizeTokenAccess(token, ctx); + newCtx = ctx; + _setPoolConnection(pool, token, currentContext.msgSender, true, true, currentContext); + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function tryConnectPoolFor(ISuperfluidPool pool, address memberAddr, bytes calldata ctx) + external + override + returns (bool success, bytes memory newCtx) { ISuperfluidToken token = pool.superToken(); ISuperfluid.Context memory currentContext = AgreementLibrary.authorizeTokenAccess(token, ctx); - address msgSender = currentContext.msgSender; + // Only the pool admin is allowed to do this + if (currentContext.msgSender != pool.admin()) { + revert GDA_NOT_POOL_ADMIN(); + } + newCtx = ctx; + success = _setPoolConnection(pool, token, memberAddr, true, false, currentContext); + } + + /// @inheritdoc IGeneralDistributionAgreementV1 + function disconnectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) { + ISuperfluidToken token = pool.superToken(); + ISuperfluid.Context memory currentContext = AgreementLibrary.authorizeTokenAccess(token, ctx); newCtx = ctx; - bool isConnected = token.isPoolMemberConnected(this, pool, msgSender); - if (doConnect != isConnected) { - assert( - SuperfluidPool(address(pool)).operatorConnectMember( - msgSender, doConnect, uint32(currentContext.timestamp) - ) - ); + _setPoolConnection(pool, token, currentContext.msgSender, false, true /* ignored */, currentContext); + } + + + // @note setPoolConnection function naming + function _setPoolConnection( + ISuperfluidPool pool, + ISuperfluidToken token, + address memberAddr, + bool doConnect, + bool useAllSlots, + ISuperfluid.Context memory currentContext + ) + internal + returns (bool success) + { + bool isConnected = token.isPoolMemberConnected(this, pool, memberAddr); + if (doConnect != isConnected) { if (doConnect) { - uint32 poolSlotId = - _findAndFillPoolConnectionsBitmap(token, msgSender, bytes32(uint256(uint160(address(pool))))); + uint32 poolSlotId = useAllSlots + ? _findAndFillPoolConnectionsBitmap(token, memberAddr, bytes32(uint256(uint160(address(pool))))) + : _tryFindAndFillPoolConnectionsBitmap( + token, memberAddr, bytes32(uint256(uint160(address(pool)))), MAX_POOL_AUTO_CONNECT_SLOTS + ); + + // only if we operate with limited slots, can it fail without revert. + if (poolSlotId == type(uint32).max) { + return false; + } 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 @@ -897,6 +937,27 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi ); } + // allow to specify custom slot nr, return type(uint32).max if no slot is found + function _tryFindAndFillPoolConnectionsBitmap( + ISuperfluidToken token, + address poolMember, + bytes32 poolID, + uint32 maxSlots + ) + private + returns (uint32 slotId) + { + (uint32[] memory slotIds, ) = SlotsBitmapLibrary.listData( + token, poolMember, _POOL_SUBS_BITMAP_STATE_SLOT_ID, _POOL_CONNECTIONS_DATA_STATE_SLOT_ID_START + ); + if (slotIds.length >= maxSlots) { + return type(uint32).max; + } + 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); } diff --git a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol index d3eb2ea33b..4d2773ca01 100644 --- a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol @@ -214,6 +214,17 @@ 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. + /// @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 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/foundry.toml b/packages/ethereum-contracts/foundry.toml index df8b2e2338..2e0330617d 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/test/foundry/FoundrySuperfluidTester.t.sol b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol index 2af3bc4e5a..4ce476057b 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; @@ -1204,7 +1204,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 +1220,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 +1256,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" ); @@ -1690,10 +1725,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..880b5b5346 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -694,7 +694,7 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste assertEq(poolAdjustmentFlowRate, 0, "GDAv1.t: Pool adjustment rate is non-zero"); } - function testDistributeFlowToUnconnectedMembers( + function skip_testDistributeFlowToUnconnectedMembers( uint64[5] memory memberUnits, int32 flowRate, uint16 warpTime, @@ -971,8 +971,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,6 +988,64 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste assertEq(claimableAfter2, 0, "Connected member claimable amount should be 0"); } + + function testAdminConnect(address member, uint128 units, uint64 distributionAmount) public { + vm.assume(member != address(0)); + vm.assume(member != address(freePool)); + 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"); + + // update units to 0, this is supposed to disconnect the pool + freePool.updateMemberUnits(member, 0); + assertEq(freePool.getUnits(member), 0); + + //assertEq(sf.gda.isMemberConnected(freePool, member), false, "member should be (auto)disconnected"); + } + + 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"); + } + } + } + /*////////////////////////////////////////////////////////////////////////// Assertion Functions //////////////////////////////////////////////////////////////////////////*/ From cd864bdd6d687b7f127f3e28024f18bc0b936683 Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 17 Jul 2025 12:50:29 +0200 Subject: [PATCH 10/52] add mechanism for opting out of autoconnect using SimpleACL --- .../gdav1/GeneralDistributionAgreementV1.sol | 23 +++++++++- .../gdav1/IGeneralDistributionAgreementV1.sol | 8 +++- .../contracts/libs/SlotsBitmapLibrary.sol | 4 +- .../SuperfluidFrameworkDeploymentSteps.t.sol | 9 ++++ .../gdav1/GeneralDistributionAgreement.t.sol | 45 +++++++++++++++++-- 5 files changed, 81 insertions(+), 8 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index 195c61b9b9..55da7e1938 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, @@ -51,6 +51,9 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi // @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; + bytes32 constant public ACL_POOL_CONNECT_EXCLUSIVE_ROLE = keccak256("ACL_POOL_CONNECT_EXCLUSIVE_ROLE"); + bytes32 constant public ACL_POOL_CONNECT_EXCLUSIVE_ROLE_ADMIN = keccak256("ACL_POOL_CONNECT_EXCLUSIVE_ROLE_ADMIN"); + /// @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 @@ -329,7 +332,23 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi revert GDA_NOT_POOL_ADMIN(); } newCtx = ctx; - success = _setPoolConnection(pool, token, memberAddr, true, false, currentContext); + + // 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 = _setPoolConnection(pool, token, memberAddr, true, false, currentContext); + } + } + + 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 diff --git a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol index 4d2773ca01..51cd843986 100644 --- a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol @@ -214,7 +214,8 @@ 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. + /// @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 @@ -225,6 +226,11 @@ abstract contract IGeneralDistributionAgreementV1 is ISuperAgreement { virtual returns (bool success, bytes memory newCtx); + /// @notice Lets accounts deny or allow 3rd parties to connect them to pools. + /// By default, this permission is given (except by accounts which are Super Apps). + /// @param allow true to allow, 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..c5cc9349f9 100644 --- a/packages/ethereum-contracts/contracts/libs/SlotsBitmapLibrary.sol +++ b/packages/ethereum-contracts/contracts/libs/SlotsBitmapLibrary.sol @@ -12,8 +12,8 @@ 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 { diff --git a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol index 163713169e..616c2e5b8a 100644 --- a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol +++ b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol @@ -194,6 +194,15 @@ contract SuperfluidFrameworkDeploymentSteps { gdaV1Logic.superfluidPoolBeacon().upgradeTo(address(superfluidPoolLogic)); gdaV1Logic.superfluidPoolBeacon().transferOwnership(address(host)); } + + SimpleACL(address(host.getSimpleACL())).setRoleAdmin( + gdaV1.ACL_POOL_CONNECT_EXCLUSIVE_ROLE(), + gdaV1.ACL_POOL_CONNECT_EXCLUSIVE_ROLE_ADMIN() + ); + SimpleACL(address(host.getSimpleACL())).grantRole( + gdaV1.ACL_POOL_CONNECT_EXCLUSIVE_ROLE_ADMIN(), + address(gdaV1) + ); } else if (step == 3) {// PERIPHERAL CONTRACTS: NFT Proxy and Logic { poolAdminNFT = PoolAdminNFT(address(ProxyDeployerLibrary.deployUUPSProxy())); 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 880b5b5346..6fc981b589 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 { @@ -988,7 +989,6 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste assertEq(claimableAfter2, 0, "Connected member claimable amount should be 0"); } - function testAdminConnect(address member, uint128 units, uint64 distributionAmount) public { vm.assume(member != address(0)); vm.assume(member != address(freePool)); @@ -1019,8 +1019,7 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste // update units to 0, this is supposed to disconnect the pool freePool.updateMemberUnits(member, 0); assertEq(freePool.getUnits(member), 0); - - //assertEq(sf.gda.isMemberConnected(freePool, member), false, "member should be (auto)disconnected"); + vm.stopPrank(); } function testAutoConnectSlotLimit() public { @@ -1043,9 +1042,49 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste assertEq(success, false, "success != false"); assertEq(sf.gda.isMemberConnected(pool, bob), false, "bob should not be (auto)connected"); } + vm.stopPrank(); } } + function testConnectPermissioning() 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(); + } + /*////////////////////////////////////////////////////////////////////////// Assertion Functions //////////////////////////////////////////////////////////////////////////*/ From 88e17f35973119eaf22213ebf5e38396317ecb65 Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 17 Jul 2025 13:05:44 +0200 Subject: [PATCH 11/52] revert forge-std upgrade --- lib/forge-std | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/forge-std b/lib/forge-std index 60acb7aaad..07263d193d 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 60acb7aaadcce2d68e52986a0a66fe79f07d138f +Subproject commit 07263d193d621c4b2b0ce8b4d54af58f6957d97d From 8db285b1802b9f84a9813f87b4566e73de369b52 Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 17 Jul 2025 13:19:28 +0200 Subject: [PATCH 12/52] simplification --- .../gdav1/GeneralDistributionAgreementV1.sol | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index 55da7e1938..90683f231d 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -376,17 +376,20 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi if (doConnect != isConnected) { if (doConnect) { - uint32 poolSlotId = useAllSlots - ? _findAndFillPoolConnectionsBitmap(token, memberAddr, bytes32(uint256(uint160(address(pool))))) - : _tryFindAndFillPoolConnectionsBitmap( - token, memberAddr, bytes32(uint256(uint160(address(pool)))), MAX_POOL_AUTO_CONNECT_SLOTS + if (!useAllSlots) { + // check if we're below the slot limit for autoconnect + (uint32[] memory slotIds, ) = SlotsBitmapLibrary.listData( + token, memberAddr, _POOL_SUBS_BITMAP_STATE_SLOT_ID, _POOL_CONNECTIONS_DATA_STATE_SLOT_ID_START ); - - // only if we operate with limited slots, can it fail without revert. - if (poolSlotId == type(uint32).max) { - return false; + if (slotIds.length >= MAX_POOL_AUTO_CONNECT_SLOTS) { + return false; + } } + uint32 poolSlotId = _findAndFillPoolConnectionsBitmap( + token, memberAddr, bytes32(uint256(uint160(address(pool)))) + ); + token.createPoolConnectivity (memberAddr, GDAv1StorageLib.PoolConnectivity({ slotId: poolSlotId, pool: pool })); } else { @@ -956,27 +959,6 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi ); } - // allow to specify custom slot nr, return type(uint32).max if no slot is found - function _tryFindAndFillPoolConnectionsBitmap( - ISuperfluidToken token, - address poolMember, - bytes32 poolID, - uint32 maxSlots - ) - private - returns (uint32 slotId) - { - (uint32[] memory slotIds, ) = SlotsBitmapLibrary.listData( - token, poolMember, _POOL_SUBS_BITMAP_STATE_SLOT_ID, _POOL_CONNECTIONS_DATA_STATE_SLOT_ID_START - ); - if (slotIds.length >= maxSlots) { - return type(uint32).max; - } - 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); } From 101bdfc10a8abe87b8edc296bd7901081ed14d54 Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 17 Jul 2025 22:50:10 +0200 Subject: [PATCH 13/52] don't restrict autoconnect to pool admin, more testing --- .../autowrap/foundry.toml | 2 +- .../scheduler/foundry.toml | 2 +- .../gdav1/GeneralDistributionAgreementV1.sol | 33 +++++++--------- .../foundry/FoundrySuperfluidTester.t.sol | 38 ++++++++++++++----- .../gdav1/GeneralDistributionAgreement.t.sol | 19 +++++----- packages/solidity-semantic-money/foundry.toml | 2 +- 6 files changed, 55 insertions(+), 41 deletions(-) diff --git a/packages/automation-contracts/autowrap/foundry.toml b/packages/automation-contracts/autowrap/foundry.toml index bdf6e6f602..f4f292602f 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.23" -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 33fe841026..db88ebe9c4 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.23" -evm_version = 'paris' +evm_version = 'shanghai' optimizer = true optimizer_runs = 200 remappings = [ diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index 90683f231d..554922cf33 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -313,10 +313,8 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi /// @inheritdoc IGeneralDistributionAgreementV1 function connectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) { - ISuperfluidToken token = pool.superToken(); - ISuperfluid.Context memory currentContext = AgreementLibrary.authorizeTokenAccess(token, ctx); newCtx = ctx; - _setPoolConnection(pool, token, currentContext.msgSender, true, true, currentContext); + _setPoolConnection(pool, address(0), true, true, ctx); } /// @inheritdoc IGeneralDistributionAgreementV1 @@ -325,12 +323,6 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi override returns (bool success, bytes memory newCtx) { - ISuperfluidToken token = pool.superToken(); - ISuperfluid.Context memory currentContext = AgreementLibrary.authorizeTokenAccess(token, ctx); - // Only the pool admin is allowed to do this - if (currentContext.msgSender != pool.admin()) { - revert GDA_NOT_POOL_ADMIN(); - } newCtx = ctx; // check if the member has opted out of autoconnect @@ -338,7 +330,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi if (simpleACL.hasRole(ACL_POOL_CONNECT_EXCLUSIVE_ROLE, memberAddr)) { success = false; } else { - success = _setPoolConnection(pool, token, memberAddr, true, false, currentContext); + success = _setPoolConnection(pool, memberAddr, true, false, ctx); } } @@ -353,30 +345,33 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi /// @inheritdoc IGeneralDistributionAgreementV1 function disconnectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) { - ISuperfluidToken token = pool.superToken(); - ISuperfluid.Context memory currentContext = AgreementLibrary.authorizeTokenAccess(token, ctx); newCtx = ctx; - _setPoolConnection(pool, token, currentContext.msgSender, false, true /* ignored */, currentContext); + _setPoolConnection(pool, address(0), false, true /* ignored */, ctx); } - - // @note setPoolConnection function naming + // @note memberAddr has override semantics - if set to address(0), it will be set to the msgSender function _setPoolConnection( ISuperfluidPool pool, - ISuperfluidToken token, address memberAddr, bool doConnect, - bool useAllSlots, - ISuperfluid.Context memory currentContext + bool onlyAutoConnectSlots, + bytes memory ctx ) internal returns (bool success) { + ISuperfluidToken token = pool.superToken(); + ISuperfluid.Context memory currentContext = AgreementLibrary.authorizeTokenAccess(token, ctx); + + if (memberAddr == address(0)) { + memberAddr = currentContext.msgSender; + } + bool isConnected = token.isPoolMemberConnected(this, pool, memberAddr); if (doConnect != isConnected) { if (doConnect) { - if (!useAllSlots) { + if (onlyAutoConnectSlots) { // check if we're below the slot limit for autoconnect (uint32[] memory slotIds, ) = SlotsBitmapLibrary.listData( token, memberAddr, _POOL_SUBS_BITMAP_STATE_SLOT_ID, _POOL_CONNECTIONS_DATA_STATE_SLOT_ID_START diff --git a/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol index 4ce476057b..fc7e28cabf 100644 --- a/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol +++ b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol @@ -1324,20 +1324,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(); @@ -1345,12 +1363,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 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 6fc981b589..b260bf2190 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -989,7 +989,7 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste assertEq(claimableAfter2, 0, "Connected member claimable amount should be 0"); } - function testAdminConnect(address member, uint128 units, uint64 distributionAmount) public { + function testAutoConnect(address member, uint128 units, uint64 distributionAmount) public { vm.assume(member != address(0)); vm.assume(member != address(freePool)); vm.assume(units > 0); @@ -1015,10 +1015,6 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste assertEq(superToken.balanceOf(member), balanceBefore + expectedAmount, "balance != distributionAmount"); assertEq(freePool.getClaimable(member, uint32(block.timestamp)), 0, "claimable != 0"); - - // update units to 0, this is supposed to disconnect the pool - freePool.updateMemberUnits(member, 0); - assertEq(freePool.getUnits(member), 0); vm.stopPrank(); } @@ -1046,7 +1042,7 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste } } - function testConnectPermissioning() public { + function testAutoConnectPermissioning() public { vm.startPrank(bob); sf.gda.setConnectPermission(false); vm.stopPrank(); @@ -1091,8 +1087,8 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste 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 } @@ -1104,7 +1100,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]; @@ -1140,6 +1136,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/solidity-semantic-money/foundry.toml b/packages/solidity-semantic-money/foundry.toml index f7b6d1bbd8..d26228c096 100644 --- a/packages/solidity-semantic-money/foundry.toml +++ b/packages/solidity-semantic-money/foundry.toml @@ -4,7 +4,7 @@ src = 'packages/solidity-semantic-money/src' out = 'packages/solidity-semantic-money/out/default' cache_path = 'packages/solidity-semantic-money/out/default.cache' solc_version = '0.8.26' -evm_version = 'paris' # no PUSH0 for now +evm_version = 'shanghai' deny_warnings = true optimizer = true optimizer_runs = 200 From 701ea337fa16c3b5dee9d4f78bf3cbadf5497276 Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 17 Jul 2025 22:59:57 +0200 Subject: [PATCH 14/52] fix stack too deep? --- packages/solidity-semantic-money/foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/solidity-semantic-money/foundry.toml b/packages/solidity-semantic-money/foundry.toml index d26228c096..4a2aedcbfc 100644 --- a/packages/solidity-semantic-money/foundry.toml +++ b/packages/solidity-semantic-money/foundry.toml @@ -4,7 +4,7 @@ src = 'packages/solidity-semantic-money/src' out = 'packages/solidity-semantic-money/out/default' cache_path = 'packages/solidity-semantic-money/out/default.cache' solc_version = '0.8.26' -evm_version = 'shanghai' +evm_version = 'paris' deny_warnings = true optimizer = true optimizer_runs = 200 From 4dce3b740ac9db3706846acae7e4758c965f5ab1 Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 17 Jul 2025 23:30:12 +0200 Subject: [PATCH 15/52] fix flag --- .../agreements/gdav1/GeneralDistributionAgreementV1.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index 554922cf33..27f3ea141c 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -314,7 +314,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi /// @inheritdoc IGeneralDistributionAgreementV1 function connectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) { newCtx = ctx; - _setPoolConnection(pool, address(0), true, true, ctx); + _setPoolConnection(pool, address(0), true, false, ctx); } /// @inheritdoc IGeneralDistributionAgreementV1 @@ -330,7 +330,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi if (simpleACL.hasRole(ACL_POOL_CONNECT_EXCLUSIVE_ROLE, memberAddr)) { success = false; } else { - success = _setPoolConnection(pool, memberAddr, true, false, ctx); + success = _setPoolConnection(pool, memberAddr, true, true, ctx); } } From 5a603b2d9e1955c77dfe2bc9696c7b51721fafa7 Mon Sep 17 00:00:00 2001 From: didi Date: Fri, 18 Jul 2025 00:00:58 +0200 Subject: [PATCH 16/52] more shanghai --- packages/ethereum-contracts/hardhat.config.ts | 2 +- packages/ethereum-contracts/truffle-config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ethereum-contracts/hardhat.config.ts b/packages/ethereum-contracts/hardhat.config.ts index 91301ba2d1..2f666ba121 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/truffle-config.js b/packages/ethereum-contracts/truffle-config.js index d89b4a0e67..91283d7279 100644 --- a/packages/ethereum-contracts/truffle-config.js +++ b/packages/ethereum-contracts/truffle-config.js @@ -391,7 +391,7 @@ const E = (module.exports = { // 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", }, }, }, From d3bfd9559d3789f43feb0ed70bf438e2143db92e Mon Sep 17 00:00:00 2001 From: didi Date: Fri, 18 Jul 2025 12:07:16 +0200 Subject: [PATCH 17/52] updated CHANGELOG --- packages/ethereum-contracts/CHANGELOG.md | 4 ++++ .../contracts/agreements/gdav1/SuperfluidPool.sol | 4 ++-- .../agreements/gdav1/SuperfluidPoolDeployerLibrary.sol | 4 ++-- packages/ethereum-contracts/truffle-config.js | 2 -- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index 7747002294..9cdef08e39 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -5,11 +5,15 @@ 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. +- 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/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..2ccf4438ac 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol @@ -11,8 +11,8 @@ library SuperfluidPoolDeployerLibrary { 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, diff --git a/packages/ethereum-contracts/truffle-config.js b/packages/ethereum-contracts/truffle-config.js index 91283d7279..02207399ec 100644 --- a/packages/ethereum-contracts/truffle-config.js +++ b/packages/ethereum-contracts/truffle-config.js @@ -389,8 +389,6 @@ 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: "shanghai", }, }, From d6c913595eac061a7af91f4e9e3bc33ac4f6e026 Mon Sep 17 00:00:00 2001 From: didi Date: Fri, 18 Jul 2025 17:59:19 +0200 Subject: [PATCH 18/52] undo disable test --- .../foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b260bf2190..9277f59231 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -695,7 +695,7 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste assertEq(poolAdjustmentFlowRate, 0, "GDAv1.t: Pool adjustment rate is non-zero"); } - function skip_testDistributeFlowToUnconnectedMembers( + function testDistributeFlowToUnconnectedMembers( uint64[5] memory memberUnits, int32 flowRate, uint16 warpTime, From 2aa5ccdc79e5f0bf97098a3d7cda6135669f3c55 Mon Sep 17 00:00:00 2001 From: didi Date: Fri, 18 Jul 2025 18:14:53 +0200 Subject: [PATCH 19/52] added tryConnectPoolFor to SuperTokenV1Library --- .../contracts/apps/SuperTokenV1Library.sol | 18 ++++++++++++++++++ .../foundry/apps/SuperTokenV1Library.t.sol | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol b/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol index 529ec77d57..3279b4828e 100644 --- a/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol +++ b/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol @@ -1415,6 +1415,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. diff --git a/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol b/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol index a7560519e3..cb464784bb 100644 --- a/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol +++ b/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol @@ -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 From b4e6342b2107f50d8ee4a968a51d0c5a849672da Mon Sep 17 00:00:00 2001 From: didi Date: Fri, 18 Jul 2025 18:34:35 +0200 Subject: [PATCH 20/52] CFASuperAppBase: disable autoconnect in constructor --- .../contracts/apps/CFASuperAppBase.sol | 18 +++++++++++++++++- .../gdav1/IGeneralDistributionAgreementV1.sol | 5 ++--- 2 files changed, 19 insertions(+), 4 deletions(-) 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/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol index 51cd843986..8f438ccdb0 100644 --- a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol @@ -226,9 +226,8 @@ abstract contract IGeneralDistributionAgreementV1 is ISuperAgreement { virtual returns (bool success, bytes memory newCtx); - /// @notice Lets accounts deny or allow 3rd parties to connect them to pools. - /// By default, this permission is given (except by accounts which are Super Apps). - /// @param allow true to allow, false to deny + /// @notice Lets accounts deny or allow 3rd parties to connect them to pools. The default is to allow. + /// @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`. From c1959234b21801561abd93e52bd19a606ece02cf Mon Sep 17 00:00:00 2001 From: didi Date: Fri, 18 Jul 2025 19:35:50 +0200 Subject: [PATCH 21/52] set admin roles in deploy script --- .../gdav1/GeneralDistributionAgreementV1.sol | 2 +- .../SuperfluidFrameworkDeploymentSteps.t.sol | 5 ++-- .../ops-scripts/deploy-framework.js | 25 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index 27f3ea141c..a15c4fb017 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -51,8 +51,8 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi // @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"); - bytes32 constant public ACL_POOL_CONNECT_EXCLUSIVE_ROLE_ADMIN = keccak256("ACL_POOL_CONNECT_EXCLUSIVE_ROLE_ADMIN"); /// @dev Pool member state slot id for storing subs bitmap uint256 private constant _POOL_SUBS_BITMAP_STATE_SLOT_ID = 1; diff --git a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol index 616c2e5b8a..4714576620 100644 --- a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol +++ b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol @@ -195,12 +195,13 @@ contract SuperfluidFrameworkDeploymentSteps { 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(), - gdaV1.ACL_POOL_CONNECT_EXCLUSIVE_ROLE_ADMIN() + aclPoolConnectExclusiveRoleAdmin ); SimpleACL(address(host.getSimpleACL())).grantRole( - gdaV1.ACL_POOL_CONNECT_EXCLUSIVE_ROLE_ADMIN(), + aclPoolConnectExclusiveRoleAdmin, address(gdaV1) ); } else if (step == 3) {// PERIPHERAL CONTRACTS: NFT Proxy and Logic 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) From bd1d158147eba88b4acb1e9f5e7eb8af2878d410 Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Tue, 22 Jul 2025 11:29:39 +0300 Subject: [PATCH 22/52] solidity semantic money to shanghai evm --- packages/solidity-semantic-money/foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/solidity-semantic-money/foundry.toml b/packages/solidity-semantic-money/foundry.toml index 4a2aedcbfc..d26228c096 100644 --- a/packages/solidity-semantic-money/foundry.toml +++ b/packages/solidity-semantic-money/foundry.toml @@ -4,7 +4,7 @@ src = 'packages/solidity-semantic-money/src' out = 'packages/solidity-semantic-money/out/default' cache_path = 'packages/solidity-semantic-money/out/default.cache' solc_version = '0.8.26' -evm_version = 'paris' +evm_version = 'shanghai' deny_warnings = true optimizer = true optimizer_runs = 200 From c76c167332439b40c08165316110bc86c77ebda1 Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Tue, 22 Jul 2025 11:41:58 +0300 Subject: [PATCH 23/52] added countUsedSlots to SlotsBitmapLibrary --- .../contracts/libs/SlotsBitmapLibrary.sol | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/packages/ethereum-contracts/contracts/libs/SlotsBitmapLibrary.sol b/packages/ethereum-contracts/contracts/libs/SlotsBitmapLibrary.sol index c5cc9349f9..a38e4eea14 100644 --- a/packages/ethereum-contracts/contracts/libs/SlotsBitmapLibrary.sol +++ b/packages/ethereum-contracts/contracts/libs/SlotsBitmapLibrary.sol @@ -19,13 +19,13 @@ 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; + } + } } From 64484aeced94bf89fba1d5ad7ab1dccc23116e64 Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Tue, 22 Jul 2025 11:42:14 +0300 Subject: [PATCH 24/52] GDAv1: use SlotsBitmapLibrary --- .../agreements/gdav1/GeneralDistributionAgreementV1.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index a15c4fb017..f04643fd74 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -318,7 +318,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi } /// @inheritdoc IGeneralDistributionAgreementV1 - function tryConnectPoolFor(ISuperfluidPool pool, address memberAddr, bytes calldata ctx) + function tryConnectPoolFor(ISuperfluidPool pool, address memberAddr, bytes calldata ctx) external override returns (bool success, bytes memory newCtx) @@ -373,10 +373,10 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi if (doConnect) { if (onlyAutoConnectSlots) { // check if we're below the slot limit for autoconnect - (uint32[] memory slotIds, ) = SlotsBitmapLibrary.listData( - token, memberAddr, _POOL_SUBS_BITMAP_STATE_SLOT_ID, _POOL_CONNECTIONS_DATA_STATE_SLOT_ID_START + uint256 nUsedSlots = SlotsBitmapLibrary.countUsedSlots( + token, memberAddr, _POOL_SUBS_BITMAP_STATE_SLOT_ID ); - if (slotIds.length >= MAX_POOL_AUTO_CONNECT_SLOTS) { + if (nUsedSlots > MAX_POOL_AUTO_CONNECT_SLOTS) { return false; } } From d8ea0dc1f27fb1a78df2f8e6c9a30eb75c082e79 Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Tue, 22 Jul 2025 11:49:12 +0300 Subject: [PATCH 25/52] added some asserts to SlotsBitmapLibraryPropertyTest._listData --- .../test/foundry/libs/SlotsBitmapLibrary.prop.t.sol | 5 +++++ 1 file changed, 5 insertions(+) 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"); } /** From 79109ec732bb9c191ebe40cf6c4342ccfe6af642 Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Tue, 22 Jul 2025 12:03:33 +0300 Subject: [PATCH 26/52] revert solidity semantic money to paris evm --- packages/solidity-semantic-money/foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/solidity-semantic-money/foundry.toml b/packages/solidity-semantic-money/foundry.toml index d26228c096..4a2aedcbfc 100644 --- a/packages/solidity-semantic-money/foundry.toml +++ b/packages/solidity-semantic-money/foundry.toml @@ -4,7 +4,7 @@ src = 'packages/solidity-semantic-money/src' out = 'packages/solidity-semantic-money/out/default' cache_path = 'packages/solidity-semantic-money/out/default.cache' solc_version = '0.8.26' -evm_version = 'shanghai' +evm_version = 'paris' deny_warnings = true optimizer = true optimizer_runs = 200 From d57884275374cb8891a121b89c4bcabe4b77e62b Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Tue, 22 Jul 2025 12:07:55 +0300 Subject: [PATCH 27/52] fix testTryConnectPoolFor --- .../test/foundry/apps/SuperTokenV1Library.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol b/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol index cb464784bb..e93d8db0ce 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 }) ); @@ -457,7 +457,7 @@ contract SuperTokenV1LibraryTest is FoundrySuperfluidTester { bool success = superToken.tryConnectPoolFor(pool, bob); vm.stopPrank(); - if (i < sf.gda.MAX_POOL_AUTO_CONNECT_SLOTS()) { + 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 { From dff96ab882686044a4c82b7ee07a1e32c422bfd7 Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Tue, 22 Jul 2025 12:48:41 +0300 Subject: [PATCH 28/52] fix testAutoConnectSlotLimit --- .../foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9277f59231..acd83015bb 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -1031,7 +1031,7 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste new bytes(0) ); (bool success, ) = abi.decode(ret, (bool, bytes)); - if (i < sf.gda.MAX_POOL_AUTO_CONNECT_SLOTS()) { + 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 { From c855ecf871d9a2670fff9e65f91bbb9be9c9043c Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Wed, 23 Jul 2025 10:59:16 +0300 Subject: [PATCH 29/52] update flake inputs --- flake.lock | 26 +++++++++++++------------- flake.nix | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/flake.lock b/flake.lock index a5f9725a26..053374114d 100644 --- a/flake.lock +++ b/flake.lock @@ -28,16 +28,16 @@ ] }, "locked": { - "lastModified": 1748682694, - "narHash": "sha256-vun3iVnpk5cU6HZsOjpHRpeo9/ztAQWgFOMLR5IyOHE=", + "lastModified": 1752867797, + "narHash": "sha256-oT129SDSr7SI9ThTd6ZbpmShh5f2tzUH3S4hl6c5/7w=", "owner": "shazow", "repo": "foundry.nix", - "rev": "cefa65c2e3c77e9ee035e2a23188799710bb7cdb", + "rev": "d4445852933ab5bc61ca532cb6c5d3276d89c478", "type": "github" }, "original": { "owner": "shazow", - "ref": "cefa65c", + "ref": "stable", "repo": "foundry.nix", "type": "github" } @@ -68,11 +68,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1746549287, - "narHash": "sha256-lzkSxKv3GwWQ+18zb1YoHCEtFSGIPj0sAMqtQjNLihQ=", + "lastModified": 1753151930, + "narHash": "sha256-XSQy6wRKHhRe//iVY5lS/ZpI/Jn6crWI8fQzl647wCg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f90d0af5db814e870b2ad1aebc4e8924a30c53dd", + "rev": "83e677f31c84212343f4cc553bab85c2efcad60a", "type": "github" }, "original": { @@ -102,11 +102,11 @@ "solc-macos-amd64-list-json": "solc-macos-amd64-list-json" }, "locked": { - "lastModified": 1742758229, - "narHash": "sha256-FrU9rhab/0vOjjeFoQF+Ej43zRLv3enUIYjgLrH3Gd8=", + "lastModified": 1748780655, + "narHash": "sha256-mradCdMvjXwKd7kVFACB/d1CP2LLCyEgUu4vJCSzNLU=", "owner": "hellwolf", "repo": "solc.nix", - "rev": "6885b61bac89da19a6e3c70b89fdd592e2cef884", + "rev": "3b6f3223ace5a7bc400b01a434d86bb1cb2593fb", "type": "github" }, "original": { @@ -118,13 +118,13 @@ "solc-macos-amd64-list-json": { "flake": false, "locked": { - "narHash": "sha256-U5ckttxwKO13gIKggel6iybG5oTDbSidPR5nH3Gs+kY=", + "narHash": "sha256-AvITkfpNYgCypXuLJyqco0li+unVw39BAfdOZvd/SPE=", "type": "file", - "url": "https://github.com/ethereum/solc-bin/raw/30a3695/macosx-amd64/list.json" + "url": "https://github.com/ethereum/solc-bin/raw/26fc3fd/macosx-amd64/list.json" }, "original": { "type": "file", - "url": "https://github.com/ethereum/solc-bin/raw/30a3695/macosx-amd64/list.json" + "url": "https://github.com/ethereum/solc-bin/raw/26fc3fd/macosx-amd64/list.json" } }, "systems": { diff --git a/flake.nix b/flake.nix index bc5db0a3f4..684bac20b9 100644 --- a/flake.nix +++ b/flake.nix @@ -5,7 +5,7 @@ flake-utils.url = "github:numtide/flake-utils"; nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; foundry = { - url = "github:shazow/foundry.nix/cefa65c"; + url = "github:shazow/foundry.nix/stable"; inputs.flake-utils.follows = "flake-utils"; inputs.nixpkgs.follows = "nixpkgs"; }; From e0ca43f72b138d9c9c9f6eae81ed3646b5672def Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Wed, 23 Jul 2025 11:58:29 +0300 Subject: [PATCH 30/52] solc: 0.8.26 -> 0.8.30 --- .envrc | 2 +- flake.nix | 2 +- packages/ethereum-contracts/foundry.toml | 2 +- packages/ethereum-contracts/hardhat.config.ts | 2 +- packages/ethereum-contracts/truffle-config.js | 2 +- packages/solidity-semantic-money/foundry.toml | 2 +- packages/subgraph/hardhat.config.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.envrc b/.envrc index 49fe8d361f..c9b4271139 100644 --- a/.envrc +++ b/.envrc @@ -4,7 +4,7 @@ dotenv_if_exists || direnv status # https://direnv.net/man/direnv-stdlib.1.html # foundry to use solc.nix provided solc export FOUNDRY_OFFLINE=true -export FOUNDRY_SOLC_VERSION=`which solc-0.8.26` +export FOUNDRY_SOLC_VERSION=`which solc-0.8.30` # use flake shell # Note: diff --git a/flake.nix b/flake.nix index 684bac20b9..4e6f328d99 100644 --- a/flake.nix +++ b/flake.nix @@ -34,7 +34,7 @@ system: let minDevSolcVer = "solc_0_8_11"; # minimum solidity version used for external development - solcVer = "solc_0_8_26"; + solcVer = "solc_0_8_30"; ghcVer92 = "ghc928"; ghcVer94 = "ghc948"; diff --git a/packages/ethereum-contracts/foundry.toml b/packages/ethereum-contracts/foundry.toml index df8b2e2338..df19e26ee9 100644 --- a/packages/ethereum-contracts/foundry.toml +++ b/packages/ethereum-contracts/foundry.toml @@ -2,7 +2,7 @@ root = '../..' src = 'packages/ethereum-contracts/contracts' test = 'packages/ethereum-contracts/test/foundry' -solc_version = "0.8.26" +solc_version = "0.8.30" #deny_warnings = true ignored_error_codes = [ 1699 # assembly { selfdestruct } in contracts/mocks/SuperfluidDestructorMock.sol diff --git a/packages/ethereum-contracts/hardhat.config.ts b/packages/ethereum-contracts/hardhat.config.ts index 91301ba2d1..309fbdc786 100644 --- a/packages/ethereum-contracts/hardhat.config.ts +++ b/packages/ethereum-contracts/hardhat.config.ts @@ -93,7 +93,7 @@ function createNetworkConfig( const config: HardhatUserConfig = { solidity: { - version: "0.8.26", + version: "0.8.30", settings: { optimizer: { enabled: true, diff --git a/packages/ethereum-contracts/truffle-config.js b/packages/ethereum-contracts/truffle-config.js index d89b4a0e67..15a49f4727 100644 --- a/packages/ethereum-contracts/truffle-config.js +++ b/packages/ethereum-contracts/truffle-config.js @@ -381,7 +381,7 @@ const E = (module.exports = { // Fetch exact version from solc-bin (default: truffle's version) // If SOLC environment variable is provided, assuming it is available as "solc", use it instead. // Ref, this maybe possible in the future: https://github.com/trufflesuite/truffle/pull/6007 - version: process.env.SOLC ? "native" : "0.8.26", + version: process.env.SOLC ? "native" : "0.8.30", settings: { // See the solidity docs for advice about optimization and evmVersion optimizer: { diff --git a/packages/solidity-semantic-money/foundry.toml b/packages/solidity-semantic-money/foundry.toml index f7b6d1bbd8..9313afd96e 100644 --- a/packages/solidity-semantic-money/foundry.toml +++ b/packages/solidity-semantic-money/foundry.toml @@ -3,7 +3,7 @@ root = '../..' src = 'packages/solidity-semantic-money/src' out = 'packages/solidity-semantic-money/out/default' cache_path = 'packages/solidity-semantic-money/out/default.cache' -solc_version = '0.8.26' +solc_version = '0.8.30' evm_version = 'paris' # no PUSH0 for now deny_warnings = true optimizer = true diff --git a/packages/subgraph/hardhat.config.ts b/packages/subgraph/hardhat.config.ts index bda0b1e477..0569e12aa2 100644 --- a/packages/subgraph/hardhat.config.ts +++ b/packages/subgraph/hardhat.config.ts @@ -13,7 +13,7 @@ dotenvConfig(); */ const config: HardhatUserConfig = { solidity: { - version: "0.8.26", + version: "0.8.30", settings: { optimizer: { enabled: true, From ae36ff0b360406f1598b6ed24bc8eb7580626d20 Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Wed, 23 Jul 2025 12:21:50 +0300 Subject: [PATCH 31/52] ethereum-contracts: update CHANGELOG.md --- packages/ethereum-contracts/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index 7747002294..2fa1f22d73 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `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". ### 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). From 365b08751768463618b1cfcd547881077c2623f7 Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Wed, 23 Jul 2025 12:35:36 +0300 Subject: [PATCH 32/52] update more solc_version to 0.8.30 --- packages/automation-contracts/autowrap/foundry.toml | 2 +- packages/automation-contracts/scheduler/foundry.toml | 2 +- packages/hot-fuzz/foundry.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/automation-contracts/autowrap/foundry.toml b/packages/automation-contracts/autowrap/foundry.toml index bdf6e6f602..e79af10c38 100644 --- a/packages/automation-contracts/autowrap/foundry.toml +++ b/packages/automation-contracts/autowrap/foundry.toml @@ -2,7 +2,7 @@ root = '../../../' libs = ['lib'] src = 'packages/automation-contracts/autowrap' -solc_version = "0.8.23" +solc_version = "0.8.30" evm_version = 'paris' optimizer = true optimizer_runs = 200 diff --git a/packages/automation-contracts/scheduler/foundry.toml b/packages/automation-contracts/scheduler/foundry.toml index 33fe841026..a4e9c41259 100644 --- a/packages/automation-contracts/scheduler/foundry.toml +++ b/packages/automation-contracts/scheduler/foundry.toml @@ -2,7 +2,7 @@ root = '../../../' libs = ['lib'] src = 'packages/automation-contracts/scheduler' -solc_version = "0.8.23" +solc_version = "0.8.30" evm_version = 'paris' optimizer = true optimizer_runs = 200 diff --git a/packages/hot-fuzz/foundry.toml b/packages/hot-fuzz/foundry.toml index 1571e85714..eb05ebf501 100644 --- a/packages/hot-fuzz/foundry.toml +++ b/packages/hot-fuzz/foundry.toml @@ -1,7 +1,7 @@ [profile.default] root = '../..' src = 'packages/hot-fuzz/contracts' -solc_version = "0.8.23" +solc_version = "0.8.30" evm_version = 'shanghai' optimizer = true optimizer_runs = 200 From 7b2e30f7e4bf7d1aeaa94cf9e8ce170b7f9d63e8 Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Wed, 23 Jul 2025 13:03:00 +0300 Subject: [PATCH 33/52] revert the wrong fix --- .../agreements/gdav1/GeneralDistributionAgreementV1.sol | 2 +- .../foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol | 2 +- .../test/foundry/apps/SuperTokenV1Library.t.sol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index f04643fd74..84bf3a1533 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -376,7 +376,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi uint256 nUsedSlots = SlotsBitmapLibrary.countUsedSlots( token, memberAddr, _POOL_SUBS_BITMAP_STATE_SLOT_ID ); - if (nUsedSlots > MAX_POOL_AUTO_CONNECT_SLOTS) { + if (nUsedSlots >= MAX_POOL_AUTO_CONNECT_SLOTS) { return false; } } 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 acd83015bb..9277f59231 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -1031,7 +1031,7 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste new bytes(0) ); (bool success, ) = abi.decode(ret, (bool, bytes)); - if (i <= sf.gda.MAX_POOL_AUTO_CONNECT_SLOTS()) { + 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 { diff --git a/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol b/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol index e93d8db0ce..8983b1a162 100644 --- a/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol +++ b/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol @@ -457,7 +457,7 @@ contract SuperTokenV1LibraryTest is FoundrySuperfluidTester { bool success = superToken.tryConnectPoolFor(pool, bob); vm.stopPrank(); - if (i <= sf.gda.MAX_POOL_AUTO_CONNECT_SLOTS()) { + 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 { From cdf04820c16b627b0f06854b63c7156daa579d9f Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Wed, 23 Jul 2025 13:23:51 +0300 Subject: [PATCH 34/52] small code refactoring to _setPoolConnection --- .../gdav1/GeneralDistributionAgreementV1.sol | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index 84bf3a1533..274fa33c50 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -314,7 +314,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi /// @inheritdoc IGeneralDistributionAgreementV1 function connectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) { newCtx = ctx; - _setPoolConnection(pool, address(0), true, false, ctx); + _setPoolConnection(pool, address(0), true /* doConnect */, ctx); } /// @inheritdoc IGeneralDistributionAgreementV1 @@ -330,7 +330,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi if (simpleACL.hasRole(ACL_POOL_CONNECT_EXCLUSIVE_ROLE, memberAddr)) { success = false; } else { - success = _setPoolConnection(pool, memberAddr, true, true, ctx); + success = _setPoolConnection(pool, memberAddr, true /* doConnect */, ctx); } } @@ -346,7 +346,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi /// @inheritdoc IGeneralDistributionAgreementV1 function disconnectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) { newCtx = ctx; - _setPoolConnection(pool, address(0), false, true /* ignored */, ctx); + _setPoolConnection(pool, address(0), false /* doConnect */, ctx); } // @note memberAddr has override semantics - if set to address(0), it will be set to the msgSender @@ -354,7 +354,6 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi ISuperfluidPool pool, address memberAddr, bool doConnect, - bool onlyAutoConnectSlots, bytes memory ctx ) internal @@ -363,15 +362,18 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi ISuperfluidToken token = pool.superToken(); ISuperfluid.Context memory currentContext = AgreementLibrary.authorizeTokenAccess(token, ctx); + 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) { - if (onlyAutoConnectSlots) { + if (autoConnectForOtherMember) { // check if we're below the slot limit for autoconnect uint256 nUsedSlots = SlotsBitmapLibrary.countUsedSlots( token, memberAddr, _POOL_SUBS_BITMAP_STATE_SLOT_ID From 9b4da71f3b94ccad52421c9f62501829934847df Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 23 Jul 2025 17:01:24 +0200 Subject: [PATCH 35/52] fix tests --- .../contracts/apps/SuperTokenV1Library.sol | 7 +++++++ .../agreements/gdav1/IGeneralDistributionAgreementV1.sol | 2 +- .../test/foundry/FoundrySuperfluidTester.t.sol | 2 ++ .../agreements/gdav1/GeneralDistributionAgreement.t.sol | 2 ++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol b/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol index 3279b4828e..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 @@ -1462,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 @@ -1517,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 8f438ccdb0..e90327024a 100644 --- a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol @@ -226,7 +226,7 @@ abstract contract IGeneralDistributionAgreementV1 is ISuperAgreement { virtual returns (bool success, bytes memory newCtx); - /// @notice Lets accounts deny or allow 3rd parties to connect them to pools. The default is to allow. + /// @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; diff --git a/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol index fc7e28cabf..e680cd1748 100644 --- a/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol +++ b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol @@ -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; 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 9277f59231..71033d0b8f 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -950,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); @@ -992,6 +993,7 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste 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); From 283402abd05b7919ac72a4bb07a4f329b40f0554 Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 23 Jul 2025 18:53:30 +0200 Subject: [PATCH 36/52] fix flaky test --- lib/forge-std | 2 +- .../test/foundry/apps/CrossStreamSuperApp.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/forge-std b/lib/forge-std index 07263d193d..60acb7aaad 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 07263d193d621c4b2b0ce8b4d54af58f6957d97d +Subproject commit 60acb7aaadcce2d68e52986a0a66fe79f07d138f 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)); From c5fd4d513d02763b60ca3e828c75c9fba64d080e Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 24 Jul 2025 10:47:53 +0200 Subject: [PATCH 37/52] fix scheduler test --- .../automation-contracts/scheduler/test/FlowScheduler.t.sol | 2 +- .../scheduler/test/VestingSchedulerV2.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol b/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol index 7c1bb94efd..266eb7016e 100644 --- a/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol +++ b/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol @@ -232,7 +232,7 @@ contract FlowSchedulerTest is FoundrySuperfluidTester { superToken, alice, defaultStartDate, uint32(1000), int96(1000), defaultStartAmount, defaultStartDate + uint32(3600), "", "" ); - vm.expectRevert(0xa3eab6ac); // error CFA_ACL_OPERATOR_NO_CREATE_PERMISSIONS() -> 0xa3eab6ac + vm.expectRevert(bytes4(0xa3eab6ac)); // error CFA_ACL_OPERATOR_NO_CREATE_PERMISSIONS() -> 0xa3eab6ac vm.warp(defaultStartDate + 1000); vm.prank(admin); flowScheduler.executeCreateFlow(superToken, bob,alice,""); diff --git a/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol index 2ad00c07b6..7eff7da0c7 100644 --- a/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol @@ -200,7 +200,7 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { address superToken, address sender, address receiver - ) public { + ) public view { VestingSchedulerV2.VestingSchedule memory schedule = vestingScheduler.getVestingSchedule(superToken, sender, receiver); VestingSchedulerV2.VestingSchedule memory deletedSchedule; From 088eba38024f1b39181ba28797a9577e6e1801dc Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 24 Jul 2025 14:52:41 +0200 Subject: [PATCH 38/52] renamed to _setPoolConnectionFor --- .../agreements/gdav1/GeneralDistributionAgreementV1.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index 274fa33c50..89cfa622cd 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -314,7 +314,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi /// @inheritdoc IGeneralDistributionAgreementV1 function connectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) { newCtx = ctx; - _setPoolConnection(pool, address(0), true /* doConnect */, ctx); + _setPoolConnectionFor(pool, address(0), true /* doConnect */, ctx); } /// @inheritdoc IGeneralDistributionAgreementV1 @@ -330,7 +330,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi if (simpleACL.hasRole(ACL_POOL_CONNECT_EXCLUSIVE_ROLE, memberAddr)) { success = false; } else { - success = _setPoolConnection(pool, memberAddr, true /* doConnect */, ctx); + success = _setPoolConnectionFor(pool, memberAddr, true /* doConnect */, ctx); } } @@ -346,11 +346,11 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi /// @inheritdoc IGeneralDistributionAgreementV1 function disconnectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) { newCtx = ctx; - _setPoolConnection(pool, address(0), false /* doConnect */, 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 _setPoolConnection( + function _setPoolConnectionFor( ISuperfluidPool pool, address memberAddr, bool doConnect, From 03b81ba3769f7756836fcb040c326cad046b8efa Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 24 Jul 2025 15:10:36 +0200 Subject: [PATCH 39/52] revert if trying to connect a pool for a pool --- .../gdav1/GeneralDistributionAgreementV1.sol | 4 ++++ .../gdav1/IGeneralDistributionAgreementV1.sol | 1 + .../gdav1/GeneralDistributionAgreement.t.sol | 12 ++++++++++++ 3 files changed, 17 insertions(+) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index 89cfa622cd..c19798532e 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -325,6 +325,10 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi { 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)) { diff --git a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol index e90327024a..40d90be9ee 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 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 71033d0b8f..0c3050b781 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -1081,6 +1081,18 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste ); 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(); } /*////////////////////////////////////////////////////////////////////////// From 5652c9d685fc4bde63f6b863b3c48dcbb050873f Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 24 Jul 2025 15:16:11 +0200 Subject: [PATCH 40/52] calldata storage location for PoolConfig --- .../agreements/gdav1/GeneralDistributionAgreementV1.sol | 6 +++--- .../agreements/gdav1/IGeneralDistributionAgreementV1.sol | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index c19798532e..a0850b56fc 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -234,7 +234,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 @@ -261,7 +261,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi } /// @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) @@ -278,7 +278,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); diff --git a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol index 40d90be9ee..6c7b157eda 100644 --- a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol @@ -180,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); @@ -194,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); From 21da9df05c6bc9af607aa654e43ca3603881ba2c Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 24 Jul 2025 16:02:01 +0200 Subject: [PATCH 41/52] squeeze out a few more bytes --- .../gdav1/GeneralDistributionAgreementV1.sol | 25 +++++++++++-------- .../gdav1/SuperfluidPoolDeployerLibrary.sol | 12 +++++---- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index a0850b56fc..518f4b3adf 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -235,7 +235,9 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi ISuperfluidToken token, address admin, PoolConfig calldata config, - PoolERC20Metadata memory poolERC20Metadata + string calldata name, + string calldata symbol, + uint8 decimals ) 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(); @@ -244,7 +246,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi pool = ISuperfluidPool( address( SuperfluidPoolDeployerLibrary.deploy( - address(superfluidPoolBeacon), admin, token, config, poolERC20Metadata + address(superfluidPoolBeacon), admin, token, config, name, symbol, decimals ) ) ); @@ -266,12 +268,13 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi override returns (ISuperfluidPool pool) { - return _createPool( - token, - admin, - config, - PoolERC20Metadata("", "", 0) // use defaults specified by the implementation contract - ); + string calldata emptyStr; + assembly { + let emptyOffset := calldatasize() + emptyStr.offset := emptyOffset + emptyStr.length := 0 + } + return _createPool(token, admin, config, emptyStr, emptyStr, 0); } /// @inheritdoc IGeneralDistributionAgreementV1 @@ -279,9 +282,11 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi ISuperfluidToken token, address admin, PoolConfig calldata config, - PoolERC20Metadata memory poolERC20Metadata + PoolERC20Metadata calldata poolERC20Metadata ) external override returns (ISuperfluidPool pool) { - return _createPool(token, admin, config, poolERC20Metadata); + return _createPool( + token, admin, config, poolERC20Metadata.name, poolERC20Metadata.symbol, poolERC20Metadata.decimals + ); } /// @inheritdoc IGeneralDistributionAgreementV1 diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol index 2ccf4438ac..79a24fb275 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.23; import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol"; import { SuperfluidPool } from "./SuperfluidPool.sol"; -import { PoolConfig, PoolERC20Metadata } from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; +import { PoolConfig } from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; library SuperfluidPoolDeployerLibrary { function deploy( @@ -12,7 +12,9 @@ library SuperfluidPoolDeployerLibrary { address admin, ISuperfluidToken token, PoolConfig calldata config, - PoolERC20Metadata calldata poolERC20Metadata + string calldata name, + string calldata symbol, + uint8 decimals ) external returns (SuperfluidPool pool) { bytes memory initializeCallData = abi.encodeWithSelector( SuperfluidPool.initialize.selector, @@ -20,9 +22,9 @@ library SuperfluidPoolDeployerLibrary { token, config.transferabilityForUnitsOwner, config.distributionFromAnyAddress, - poolERC20Metadata.name, - poolERC20Metadata.symbol, - poolERC20Metadata.decimals + name, + symbol, + decimals ); BeaconProxy superfluidPoolBeaconProxy = new BeaconProxy( beacon, From 56084757299caea80761397d2d119978fd73d361 Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 24 Jul 2025 16:15:55 +0200 Subject: [PATCH 42/52] more contract size margin for GDA --- .../gdav1/GeneralDistributionAgreementV1.sol | 23 +----------------- .../gdav1/SuperfluidPoolDeployerLibrary.sol | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index 518f4b3adf..98e8cc2722 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -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"; @@ -253,11 +251,7 @@ 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); } @@ -601,21 +595,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/SuperfluidPoolDeployerLibrary.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol index 79a24fb275..e251031abc 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol @@ -5,6 +5,9 @@ import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.so import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol"; import { SuperfluidPool } from "./SuperfluidPool.sol"; import { PoolConfig } 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( @@ -32,4 +35,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)); + } + } } From 5a3639d10bf420b4597bdb77586e0238281da88f Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 24 Jul 2025 16:18:02 +0200 Subject: [PATCH 43/52] fix import --- .../agreements/gdav1/GeneralDistributionAgreementV1.prop.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a8264c0714f3bd723802b30ff37a490534acc85a Mon Sep 17 00:00:00 2001 From: crStiv Date: Sun, 27 Jul 2025 18:03:32 +0200 Subject: [PATCH 44/52] Update VestingSchedulerV2.sol --- .../scheduler/contracts/VestingSchedulerV2.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol index 89464102a6..54bba3b547 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV2.sol @@ -441,7 +441,7 @@ contract VestingSchedulerV2 is IVestingSchedulerV2, SuperAppBase { if (!disableClaimCheck && schedule.claimValidityDate != 0) revert ScheduleNotClaimed(); - // Ensure that the claming date is after the cliff/flow date and before the claim validity date + // Ensure that the claiming date is after the cliff/flow date and before the claim validity date if (schedule.cliffAndFlowDate > block.timestamp || _lteDateToExecuteCliffAndFlow(schedule) < block.timestamp) revert TimeWindowInvalid(); From 0d29e7a07c7ae3495b3e89f20f1b004b1888cefa Mon Sep 17 00:00:00 2001 From: crStiv Date: Sun, 27 Jul 2025 18:04:54 +0200 Subject: [PATCH 45/52] Update VestingSchedulerV3.sol --- .../scheduler/contracts/VestingSchedulerV3.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV3.sol b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV3.sol index d8445d1478..d726ea4197 100644 --- a/packages/automation-contracts/scheduler/contracts/VestingSchedulerV3.sol +++ b/packages/automation-contracts/scheduler/contracts/VestingSchedulerV3.sol @@ -32,7 +32,7 @@ using SuperTokenV1Library for ISuperToken; * The contract uses ERC-20 allowance and Superfluid ACL flow operator permissions * to automate the vesting on behalf of the sender. * The contract is designed to be used with an off-chain automation to execute the vesting start and end. - * The start and end executions are permisionless. + * The start and end executions are permissionless. * Execution delays are handled with token transfer compensations, but watch out for complete expiries! * @custom:metadata The official addresses and subgraphs can be found from @superfluid-finance/metadata package. */ @@ -760,7 +760,7 @@ contract VestingSchedulerV3 is IVestingSchedulerV3, IRelayRecipient { revert ScheduleNotClaimed(); } - // Ensure that the claming date is after the cliff/flow date and before the claim validity date + // Ensure that the claiming date is after the cliff/flow date and before the claim validity date if (schedule.cliffAndFlowDate > block.timestamp || _lteDateToExecuteCliffAndFlow(schedule) < block.timestamp) { revert TimeWindowInvalid(); } From 1588c53967a2d1ed846203ee785b855d0d2d0a3d Mon Sep 17 00:00:00 2001 From: crStiv Date: Sun, 27 Jul 2025 18:05:08 +0200 Subject: [PATCH 46/52] Update FlowScheduler.t.sol --- .../automation-contracts/scheduler/test/FlowScheduler.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol b/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol index 7c1bb94efd..fd1183aadb 100644 --- a/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol +++ b/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol @@ -50,7 +50,7 @@ contract FlowSchedulerTest is FoundrySuperfluidTester { bytes userData ); - /// @dev This is required by solidity for using the SupertTokenV1Library in the tester + /// @dev This is required by solidity for using the SuperTokenV1Library in the tester using SuperTokenV1Library for SuperToken; constructor() FoundrySuperfluidTester(3) {} FlowScheduler internal flowScheduler; From 7935a0e8cf2f04bcea04b22c2d837aeef01e9568 Mon Sep 17 00:00:00 2001 From: crStiv Date: Sun, 27 Jul 2025 18:06:09 +0200 Subject: [PATCH 47/52] Update InstantDistributionAgreementV1-Non-Callback.test.ts --- .../InstantDistributionAgreementV1-Non-Callback.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ethereum-contracts/test/contracts/agreements/InstantDistributionAgreementV1-Non-Callback.test.ts b/packages/ethereum-contracts/test/contracts/agreements/InstantDistributionAgreementV1-Non-Callback.test.ts index 273a819d23..e618592595 100644 --- a/packages/ethereum-contracts/test/contracts/agreements/InstantDistributionAgreementV1-Non-Callback.test.ts +++ b/packages/ethereum-contracts/test/contracts/agreements/InstantDistributionAgreementV1-Non-Callback.test.ts @@ -117,7 +117,7 @@ describe("IDAv1 | Non-Callback Tests", function () { ); }); - it("#1.1.3 publisher should fail to query non-existant index", async () => { + it("#1.1.3 publisher should fail to query non-existent index", async () => { const idata = await t.contracts.ida.getIndex( superToken.address, alice, From 027a2030c78bc8c0644a60856746e8e5690fd421 Mon Sep 17 00:00:00 2001 From: crStiv Date: Sun, 27 Jul 2025 18:06:29 +0200 Subject: [PATCH 48/52] Update FlowSchedulerResolver.t.sol --- .../scheduler/test/FlowSchedulerResolver.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/automation-contracts/scheduler/test/FlowSchedulerResolver.t.sol b/packages/automation-contracts/scheduler/test/FlowSchedulerResolver.t.sol index 573ef10a31..a51dac07a8 100644 --- a/packages/automation-contracts/scheduler/test/FlowSchedulerResolver.t.sol +++ b/packages/automation-contracts/scheduler/test/FlowSchedulerResolver.t.sol @@ -350,7 +350,7 @@ contract FlowSchedulerResolverTest is FoundrySuperfluidTester { expectUnexecutable(); } - function testDeleteNonExistantStreamAfterEndDate() public { + function testDeleteNonExistentStreamAfterEndDate() public { vm.startPrank(alice); uint32 defaultEndDate = defaultStartDate + uint32(3600); From 2ab51c9fe6ff41c62124dc9c89c9fdc8dd5aec52 Mon Sep 17 00:00:00 2001 From: crStiv Date: Sun, 27 Jul 2025 18:06:56 +0200 Subject: [PATCH 49/52] Update VestingSchedulerV2.t.sol --- .../scheduler/test/VestingSchedulerV2.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol index 2ad00c07b6..0d2718928a 100644 --- a/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol @@ -2418,7 +2418,7 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { uint32 newEndDate = type(uint32).max - 1234; - // Setting up a batch call. Superfluid Protocol will replace the emtpy context with data about the sender. That's where the sender is retrieved from. + // Setting up a batch call. Superfluid Protocol will replace the empty context with data about the sender. That's where the sender is retrieved from. ISuperfluid.Operation[] memory ops = new ISuperfluid.Operation[](1); ops[0] = ISuperfluid.Operation({ operationType: BatchOperation.OPERATION_TYPE_SUPERFLUID_CALL_APP_ACTION, From 31bf0b38e4cd993f0e69ae65270c16279531a222 Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 31 Jul 2025 09:11:33 +0200 Subject: [PATCH 50/52] no calldata magic --- .../gdav1/GeneralDistributionAgreementV1.sol | 25 ++++++++----------- .../gdav1/SuperfluidPoolDeployerLibrary.sol | 13 +++++----- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index 98e8cc2722..08b23cde33 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -233,9 +233,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi ISuperfluidToken token, address admin, PoolConfig calldata config, - string calldata name, - string calldata symbol, - uint8 decimals + PoolERC20Metadata memory poolERC20Metadata ) 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(); @@ -244,7 +242,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi pool = ISuperfluidPool( address( SuperfluidPoolDeployerLibrary.deploy( - address(superfluidPoolBeacon), admin, token, config, name, symbol, decimals + address(superfluidPoolBeacon), admin, token, config, poolERC20Metadata ) ) ); @@ -262,13 +260,12 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi override returns (ISuperfluidPool pool) { - string calldata emptyStr; - assembly { - let emptyOffset := calldatasize() - emptyStr.offset := emptyOffset - emptyStr.length := 0 - } - return _createPool(token, admin, config, emptyStr, emptyStr, 0); + return _createPool( + token, + admin, + config, + PoolERC20Metadata("", "", 0) // use defaults specified by the implementation contract + ); } /// @inheritdoc IGeneralDistributionAgreementV1 @@ -276,11 +273,9 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi ISuperfluidToken token, address admin, PoolConfig calldata config, - PoolERC20Metadata calldata poolERC20Metadata + PoolERC20Metadata memory poolERC20Metadata ) external override returns (ISuperfluidPool pool) { - return _createPool( - token, admin, config, poolERC20Metadata.name, poolERC20Metadata.symbol, poolERC20Metadata.decimals - ); + return _createPool(token, admin, config, poolERC20Metadata); } /// @inheritdoc IGeneralDistributionAgreementV1 diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol index e251031abc..a8025e8269 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol @@ -4,20 +4,19 @@ pragma solidity ^0.8.23; 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"; +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 calldata config, - string calldata name, - string calldata symbol, - uint8 decimals + PoolERC20Metadata calldata poolERC20Metadata ) external returns (SuperfluidPool pool) { bytes memory initializeCallData = abi.encodeWithSelector( SuperfluidPool.initialize.selector, @@ -25,9 +24,9 @@ library SuperfluidPoolDeployerLibrary { token, config.transferabilityForUnitsOwner, config.distributionFromAnyAddress, - name, - symbol, - decimals + poolERC20Metadata.name, + poolERC20Metadata.symbol, + poolERC20Metadata.decimals ); BeaconProxy superfluidPoolBeaconProxy = new BeaconProxy( beacon, From ce3c48a428d28dc27e338fba8fbdf9e65a294aca Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Wed, 6 Aug 2025 14:43:59 +0300 Subject: [PATCH 51/52] forge-std: v1.9.1 -> v1.10.0 --- lib/forge-std | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/forge-std b/lib/forge-std index 07263d193d..8bbcf6e3f8 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 07263d193d621c4b2b0ce8b4d54af58f6957d97d +Subproject commit 8bbcf6e3f8f62f419e5429a0bd89331c85c37824 From 71918b431d454ab734897c569c3f9a3f9c801992 Mon Sep 17 00:00:00 2001 From: "Miao, ZhiCheng" Date: Wed, 6 Aug 2025 15:03:27 +0300 Subject: [PATCH 52/52] fix automation contracts --- packages/automation-contracts/autowrap/test/Manager.t.sol | 2 +- .../automation-contracts/autowrap/test/WrapStrategy.t.sol | 4 ++-- .../automation-contracts/scheduler/test/FlowScheduler.t.sol | 2 +- .../scheduler/test/VestingSchedulerV2.t.sol | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/automation-contracts/autowrap/test/Manager.t.sol b/packages/automation-contracts/autowrap/test/Manager.t.sol index ee81cfff0b..1441948c5c 100644 --- a/packages/automation-contracts/autowrap/test/Manager.t.sol +++ b/packages/automation-contracts/autowrap/test/Manager.t.sol @@ -89,7 +89,7 @@ contract ManagerTests is FoundrySuperfluidTester { new Manager(address(sf.cfa), 2, 1); } - function testDeploymentCheckData() public { + function testDeploymentCheckData() public view { assertEq(address(manager.cfaV1()), address(sf.cfa), "manager.cfaV1 not equal"); assertEq(manager.owner(), admin, "manager.owner not equal"); assertEq(manager.minLower(), MIN_LOWER, "manager.minLower not equal"); diff --git a/packages/automation-contracts/autowrap/test/WrapStrategy.t.sol b/packages/automation-contracts/autowrap/test/WrapStrategy.t.sol index 327c8fca24..9d56150faf 100644 --- a/packages/automation-contracts/autowrap/test/WrapStrategy.t.sol +++ b/packages/automation-contracts/autowrap/test/WrapStrategy.t.sol @@ -93,14 +93,14 @@ contract WrapStrategyTests is FoundrySuperfluidTester { // SuperToken - function testSupportedSuperToken() public { + function testSupportedSuperToken() public view { assertTrue( wrapStrategy.isSupportedSuperToken(superToken), "SuperToken should be supported" ); } - function testCannotNonSupportedSuperToken() public { + function testCannotNonSupportedSuperToken() public view { assertTrue( !wrapStrategy.isSupportedSuperToken(nativeSuperToken), "Native SuperToken shouldn't be supported" diff --git a/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol b/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol index fd1183aadb..2d9d49b1dd 100644 --- a/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol +++ b/packages/automation-contracts/scheduler/test/FlowScheduler.t.sol @@ -232,7 +232,7 @@ contract FlowSchedulerTest is FoundrySuperfluidTester { superToken, alice, defaultStartDate, uint32(1000), int96(1000), defaultStartAmount, defaultStartDate + uint32(3600), "", "" ); - vm.expectRevert(0xa3eab6ac); // error CFA_ACL_OPERATOR_NO_CREATE_PERMISSIONS() -> 0xa3eab6ac + vm.expectRevert(bytes4(0xa3eab6ac)); // error CFA_ACL_OPERATOR_NO_CREATE_PERMISSIONS() -> 0xa3eab6ac vm.warp(defaultStartDate + 1000); vm.prank(admin); flowScheduler.executeCreateFlow(superToken, bob,alice,""); diff --git a/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol index 0d2718928a..73c42cb5d6 100644 --- a/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol +++ b/packages/automation-contracts/scheduler/test/VestingSchedulerV2.t.sol @@ -200,7 +200,7 @@ contract VestingSchedulerV2Tests is FoundrySuperfluidTester { address superToken, address sender, address receiver - ) public { + ) public view { VestingSchedulerV2.VestingSchedule memory schedule = vestingScheduler.getVestingSchedule(superToken, sender, receiver); VestingSchedulerV2.VestingSchedule memory deletedSchedule;