From 697b97d410d0073496ce5b4524f7714afcaacf0f Mon Sep 17 00:00:00 2001 From: didi Date: Fri, 13 Jun 2025 10:32:47 +0200 Subject: [PATCH 01/14] added increaseMemberUnits and decreaseMemberUnits to ISuperfluidPool --- .../agreements/gdav1/SuperfluidPool.sol | 20 +++++++++++++++++++ .../agreements/gdav1/ISuperfluidPool.sol | 10 ++++++++++ packages/ethereum-contracts/package.json | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index 60d586a76c..ae531f36cf 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -399,6 +399,26 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { return true; } + /// @inheritdoc ISuperfluidPool + function increaseMemberUnits(address memberAddr, uint128 addedUnits) external override returns (bool) { + if (msg.sender != admin && msg.sender != address(GDA)) revert SUPERFLUID_POOL_NOT_POOL_ADMIN_OR_GDA(); + + _updateMemberUnits(memberAddr, _getUnits(memberAddr) + addedUnits); + emit Transfer(address(0), memberAddr, addedUnits); + + return true; + } + + /// @inheritdoc ISuperfluidPool + function decreaseMemberUnits(address memberAddr, uint128 subtractedUnits) external override returns (bool) { + if (msg.sender != admin && msg.sender != address(GDA)) revert SUPERFLUID_POOL_NOT_POOL_ADMIN_OR_GDA(); + + _updateMemberUnits(memberAddr, _getUnits(memberAddr) - subtractedUnits); + emit Transfer(memberAddr, address(0), subtractedUnits); + + return true; + } + /** * @notice Checks whether or not the NFT hook can be called. * @dev A staticcall, so `POOL_MEMBER_NFT` must be a view otherwise the assumption is that it reverts diff --git a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol index 5524318c49..b36e1f633f 100644 --- a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol @@ -89,6 +89,16 @@ interface ISuperfluidPool is IERC20, IERC20Metadata { /// @param newUnits The new units for the member function updateMemberUnits(address memberAddr, uint128 newUnits) external returns (bool); + /// @notice Increases `memberAddr` ownedUnits by `addedUnits` + /// @param memberAddr The address of the member + /// @param addedUnits The additional units for the member + function increaseMemberUnits(address memberAddr, uint128 addedUnits) external returns (bool); + + /// @notice Decreases `memberAddr` ownedUnits by `subtractedUnits` + /// @param memberAddr The address of the member + /// @param subtractedUnits The units subtracted for the member + function decreaseMemberUnits(address memberAddr, uint128 subtractedUnits) external returns (bool); + /// @notice Claims the claimable balance for `memberAddr` at `block.timestamp` /// @param memberAddr The address of the member function claimAll(address memberAddr) external returns (bool); diff --git a/packages/ethereum-contracts/package.json b/packages/ethereum-contracts/package.json index 04be34089e..30627f4fad 100644 --- a/packages/ethereum-contracts/package.json +++ b/packages/ethereum-contracts/package.json @@ -1,7 +1,7 @@ { "name": "@superfluid-finance/ethereum-contracts", "description": " Ethereum contracts implementation for the Superfluid Protocol", - "version": "1.12.1", + "version": "1.12.2", "dependencies": { "@decentral.ee/web3-helpers": "0.5.3", "@nomiclabs/hardhat-ethers": "2.2.3", From 912f970fd73f71e7c67c202fcd14b27b09bc6dab Mon Sep 17 00:00:00 2001 From: didi Date: Mon, 16 Jun 2025 17:56:41 +0200 Subject: [PATCH 02/14] added burn method to PoolMemberNFT --- .../agreements/gdav1/PoolMemberNFT.sol | 20 +++++++++++++++++++ .../agreements/gdav1/IPoolMemberNFT.sol | 4 ++++ 2 files changed, 24 insertions(+) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol index 2387806555..788900fb4b 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol @@ -136,4 +136,24 @@ contract PoolMemberNFT is PoolNFTBase, IPoolMemberNFT { // emit burn of pool member token with tokenId emit Transfer(owner, address(0), tokenId); } + + /// This was added after deprecating the PoolMemberNFT. + /// It allows owners of such tokens to get rid of them + /// in case it bothers them (e.g. cluttering the wallet). + function burn(uint256 tokenId) external { + address owner = _ownerOf(tokenId); + if (msg.sender != owner) { + revert POOL_MEMBER_NFT_ONLY_OWNER(); + } + + PoolMemberNFTData storage data = _poolMemberDataByTokenId[tokenId]; + + super._burn(tokenId); + + // remove previous tokenId flow data mapping + delete _poolMemberDataByTokenId[tokenId]; + + // emit burn of pool member token with tokenId + emit Transfer(owner, address(0), tokenId); + } } diff --git a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol index 92058adb03..3c57ccf0f8 100644 --- a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol @@ -24,6 +24,7 @@ interface IPoolMemberNFT is IPoolNFTBase { error POOL_MEMBER_NFT_NO_ZERO_MEMBER(); error POOL_MEMBER_NFT_NO_UNITS(); error POOL_MEMBER_NFT_HAS_UNITS(); + error POOL_MEMBER_NFT_ONLY_OWNER(); function onCreate(address pool, address member) external; @@ -31,6 +32,9 @@ interface IPoolMemberNFT is IPoolNFTBase { function onDelete(address pool, address member) external; + /// Allows the owner to burn their token + function burn(uint256 tokenId) external; + /// View Functions /// function poolMemberDataByTokenId(uint256 tokenId) external view returns (PoolMemberNFTData memory data); From 85a30d41fa8d17daf09c31810667864752f4545f Mon Sep 17 00:00:00 2001 From: didi Date: Mon, 16 Jun 2025 17:57:10 +0200 Subject: [PATCH 03/14] added burn method to PoolMemberNFT --- .../contracts/agreements/gdav1/PoolMemberNFT.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol index 788900fb4b..a0010e5c8f 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol @@ -146,8 +146,6 @@ contract PoolMemberNFT is PoolNFTBase, IPoolMemberNFT { revert POOL_MEMBER_NFT_ONLY_OWNER(); } - PoolMemberNFTData storage data = _poolMemberDataByTokenId[tokenId]; - super._burn(tokenId); // remove previous tokenId flow data mapping From eb1b6bd276c01376c57c61f6a573bbbc106bdfcb Mon Sep 17 00:00:00 2001 From: didi Date: Mon, 16 Jun 2025 18:19:38 +0200 Subject: [PATCH 04/14] duplicate _isPool --- .../contracts/agreements/gdav1/SuperfluidPool.sol | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index ae531f36cf..7e97778d5d 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -464,7 +464,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { function _updateMemberUnits(address memberAddr, uint128 newUnits) internal returns (uint128 oldUnits) { // @note normally we keep the sanitization in the external functions, but here // this is used in both updateMemberUnits and transfer - if (GDA.isPool(superToken, memberAddr)) revert SUPERFLUID_POOL_NO_POOL_MEMBERS(); + if (_isPool(superToken, memberAddr)) revert SUPERFLUID_POOL_NO_POOL_MEMBERS(); if (memberAddr == address(0)) revert SUPERFLUID_POOL_NO_ZERO_ADDRESS(); uint32 time = uint32(ISuperfluid(superToken.getHost()).getNow()); @@ -498,6 +498,18 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { _handlePoolMemberNFT(memberAddr, newUnits); } + // copy of GDAv1._isPool which eliminates unnecessary gas cost (less external calls) + function _isPool(ISuperfluidToken token, address account) internal view returns (bool exists) { + // @note see createPool, we retrieve the isPool bit from + // UniversalIndex for this pool to determine whether the account + // is a pool + exists = ( + // solhint-disable max-line-length + (uint256(token.getAgreementStateSlot(address(this), account, 0 /*_UNIVERSAL_INDEX_STATE_SLOT_ID*/, 1)[0]) << 224) + >> 224 + ) & 1 == 1; + } + function _claimAll(address memberAddr, uint32 time) internal returns (int256 amount) { amount = getClaimable(memberAddr, time); assert(GDA.poolSettleClaim(superToken, memberAddr, (amount))); From 27225834941d01680884f0b77eb95cb5f781af30 Mon Sep 17 00:00:00 2001 From: didi Date: Mon, 16 Jun 2025 18:28:00 +0200 Subject: [PATCH 05/14] deprecate PoolMemberNFT --- .../agreements/gdav1/PoolMemberNFT.sol | 1 + .../agreements/gdav1/SuperfluidPool.sol | 46 ------------------- 2 files changed, 1 insertion(+), 46 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol index a0010e5c8f..4b664fa7e3 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol @@ -8,6 +8,7 @@ import { IGeneralDistributionAgreementV1, ISuperfluid } from "../../interfaces/s import { ISuperfluidPool } from "../../interfaces/agreements/gdav1/ISuperfluidPool.sol"; import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol"; +/// DEPRECATED - the update hooks are no longer invoked. contract PoolMemberNFT is PoolNFTBase, IPoolMemberNFT { //// Storage Variables //// diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index 7e97778d5d..a180ffda3f 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -19,11 +19,9 @@ import { } from "@superfluid-finance/solidity-semantic-money/src/SemanticMoney.sol"; import { ISuperfluid } from "../../interfaces/superfluid/ISuperfluid.sol"; import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol"; -import { ISuperToken } from "../../interfaces/superfluid/ISuperToken.sol"; import { ISuperfluidPool } from "../../interfaces/agreements/gdav1/ISuperfluidPool.sol"; import { GeneralDistributionAgreementV1 } from "../../agreements/gdav1/GeneralDistributionAgreementV1.sol"; import { BeaconProxiable } from "../../upgradability/BeaconProxiable.sol"; -import { IPoolMemberNFT } from "../../interfaces/agreements/gdav1/IPoolMemberNFT.sol"; using SafeCast for uint256; using SafeCast for int256; @@ -419,48 +417,6 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { return true; } - /** - * @notice Checks whether or not the NFT hook can be called. - * @dev A staticcall, so `POOL_MEMBER_NFT` must be a view otherwise the assumption is that it reverts - * @param token the super token that is being streamed - * @return poolMemberNFT the address returned by low level call - */ - function _canCallNFTHook(ISuperfluidToken token) internal view returns (address poolMemberNFT) { - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory data) = - address(token).staticcall(abi.encodeWithSelector(ISuperToken.POOL_MEMBER_NFT.selector)); - - if (success) { - // @note We are aware this may revert if a Custom SuperToken's - // POOL_MEMBER_NFT does not return data that can be - // decoded to an address. This would mean it was intentionally - // done by the creator of the Custom SuperToken logic and is - // fully expected to revert in that case as the author desired. - poolMemberNFT = abi.decode(data, (address)); - } - } - - function _handlePoolMemberNFT(address memberAddr, uint128 newUnits) internal { - // Pool Member NFT Logic - IPoolMemberNFT poolMemberNFT = IPoolMemberNFT(_canCallNFTHook(superToken)); - if (address(poolMemberNFT) != address(0)) { - uint256 tokenId = poolMemberNFT.getTokenId(address(this), memberAddr); - if (newUnits == 0) { - if (poolMemberNFT.poolMemberDataByTokenId(tokenId).member != address(0)) { - poolMemberNFT.onDelete(address(this), memberAddr); - } - } else { - // if not minted, we mint a new pool member nft - if (poolMemberNFT.poolMemberDataByTokenId(tokenId).member == address(0)) { - poolMemberNFT.onCreate(address(this), memberAddr); - } else { - // if minted, we update the pool member nft - poolMemberNFT.onUpdate(address(this), memberAddr); - } - } - } - } - function _updateMemberUnits(address memberAddr, uint128 newUnits) internal returns (uint128 oldUnits) { // @note normally we keep the sanitization in the external functions, but here // this is used in both updateMemberUnits and transfer @@ -494,8 +450,6 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { assert(GDA.appendIndexUpdateByPool(superToken, p, t)); } emit MemberUnitsUpdated(superToken, memberAddr, oldUnits, newUnits); - - _handlePoolMemberNFT(memberAddr, newUnits); } // copy of GDAv1._isPool which eliminates unnecessary gas cost (less external calls) From 9820c92f4bcfbdd903d6d1274cca4193315b41a2 Mon Sep 17 00:00:00 2001 From: didi Date: Mon, 16 Jun 2025 18:33:10 +0200 Subject: [PATCH 06/14] bump version to 1.13.0 --- packages/automation-contracts/autowrap/package.json | 2 +- packages/automation-contracts/scheduler/package.json | 2 +- packages/ethereum-contracts/package.json | 2 +- packages/hot-fuzz/package.json | 4 ++-- packages/js-sdk/package.json | 2 +- packages/sdk-core/package.json | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/automation-contracts/autowrap/package.json b/packages/automation-contracts/autowrap/package.json index f67209e93e..210a9aa315 100644 --- a/packages/automation-contracts/autowrap/package.json +++ b/packages/automation-contracts/autowrap/package.json @@ -4,7 +4,7 @@ "version": "0.3.0", "devDependencies": { "@openzeppelin/contracts": "^4.9.6", - "@superfluid-finance/ethereum-contracts": "^1.12.1", + "@superfluid-finance/ethereum-contracts": "^1.13.0", "@superfluid-finance/metadata": "^1.6.0" }, "license": "MIT", diff --git a/packages/automation-contracts/scheduler/package.json b/packages/automation-contracts/scheduler/package.json index c2c6ecf6b3..1d31a2e123 100644 --- a/packages/automation-contracts/scheduler/package.json +++ b/packages/automation-contracts/scheduler/package.json @@ -4,7 +4,7 @@ "version": "1.3.0", "devDependencies": { "@openzeppelin/contracts": "^4.9.6", - "@superfluid-finance/ethereum-contracts": "^1.12.1", + "@superfluid-finance/ethereum-contracts": "^1.13.0", "@superfluid-finance/metadata": "^1.6.0" }, "license": "MIT", diff --git a/packages/ethereum-contracts/package.json b/packages/ethereum-contracts/package.json index 30627f4fad..72219b4a52 100644 --- a/packages/ethereum-contracts/package.json +++ b/packages/ethereum-contracts/package.json @@ -1,7 +1,7 @@ { "name": "@superfluid-finance/ethereum-contracts", "description": " Ethereum contracts implementation for the Superfluid Protocol", - "version": "1.12.2", + "version": "1.13.0", "dependencies": { "@decentral.ee/web3-helpers": "0.5.3", "@nomiclabs/hardhat-ethers": "2.2.3", diff --git a/packages/hot-fuzz/package.json b/packages/hot-fuzz/package.json index 2deebcc373..f375ec2321 100644 --- a/packages/hot-fuzz/package.json +++ b/packages/hot-fuzz/package.json @@ -10,13 +10,13 @@ "@openzeppelin/contracts": "4.9.6" }, "devDependencies": { - "@superfluid-finance/ethereum-contracts": "^1.12.1" + "@superfluid-finance/ethereum-contracts": "^1.13.0" }, "homepage": "https://github.com/superfluid-finance/protocol-monorepo#readme", "license": "AGPL-3.0", "main": "index.js", "peerDependencies": { - "@superfluid-finance/ethereum-contracts": "1.12.1" + "@superfluid-finance/ethereum-contracts": "1.13.0" }, "repository": { "type": "git", diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index 36edba1e1b..2c04480732 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -13,7 +13,7 @@ "node-fetch": "2.7.0" }, "devDependencies": { - "@superfluid-finance/ethereum-contracts": "^1.12.1", + "@superfluid-finance/ethereum-contracts": "^1.13.0", "chai-as-promised": "^8.0.0", "webpack": "^5.94.0", "webpack-bundle-analyzer": "^4.10.2", diff --git a/packages/sdk-core/package.json b/packages/sdk-core/package.json index 5ac7f5f434..9e84c7575b 100644 --- a/packages/sdk-core/package.json +++ b/packages/sdk-core/package.json @@ -4,7 +4,7 @@ "version": "0.9.0", "bugs": "https://github.com/superfluid-finance/protocol-monorepo/issues", "dependencies": { - "@superfluid-finance/ethereum-contracts": "1.12.1", + "@superfluid-finance/ethereum-contracts": "1.13.0", "@superfluid-finance/metadata": "^1.6.0", "graphql-request": "6.1.0", "lodash": "4.17.21", From a0aed70471927c273aaee7a01cba3334737721b8 Mon Sep 17 00:00:00 2001 From: didi Date: Mon, 16 Jun 2025 18:37:50 +0200 Subject: [PATCH 07/14] change PoolMemberNFT token name --- 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 c57fab0baa..2c6709af45 100644 --- a/packages/ethereum-contracts/ops-scripts/deploy-framework.js +++ b/packages/ethereum-contracts/ops-scripts/deploy-framework.js @@ -1198,7 +1198,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( // initialize the proxy contracts with the nft names await poolAdminNFT.initialize("Pool Admin NFT", "PA"); - await poolMemberNFT.initialize("Pool Member NFT", "PM"); + await poolMemberNFT.initialize("Pool Member NFT (deprecated)", "PM"); // set the nft proxy addresses (to be consumed by the super token logic constructor) poolAdminNFTProxyAddress = poolAdminNFTProxy.address; From 2bf628a8718dad059d5f1797ea2205c4732406de Mon Sep 17 00:00:00 2001 From: didi Date: Mon, 16 Jun 2025 21:21:09 +0200 Subject: [PATCH 08/14] adjust tests --- .../agreements/gdav1/SuperfluidPool.sol | 7 +- .../foundry/FoundrySuperfluidTester.t.sol | 27 ------ .../gdav1/GeneralDistributionAgreement.t.sol | 1 - .../foundry/superfluid/PoolMemberNFT.t.sol | 82 ------------------- .../test/foundry/superfluid/PoolNFTBase.t.sol | 22 ----- 5 files changed, 4 insertions(+), 135 deletions(-) delete mode 100644 packages/ethereum-contracts/test/foundry/superfluid/PoolMemberNFT.t.sol diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index a180ffda3f..5e16abe53a 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -452,14 +452,15 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { emit MemberUnitsUpdated(superToken, memberAddr, oldUnits, newUnits); } - // copy of GDAv1._isPool which eliminates unnecessary gas cost (less external calls) + // replicates GDAv1._isPool in order to eliminate unnecessary gas cost (less external calls) function _isPool(ISuperfluidToken token, address account) internal view returns (bool exists) { + // solhint-disable var-name-mixedcase + uint256 _UNIVERSAL_INDEX_STATE_SLOT_ID = 0; // @note see createPool, we retrieve the isPool bit from // UniversalIndex for this pool to determine whether the account // is a pool exists = ( - // solhint-disable max-line-length - (uint256(token.getAgreementStateSlot(address(this), account, 0 /*_UNIVERSAL_INDEX_STATE_SLOT_ID*/, 1)[0]) << 224) + (uint256(token.getAgreementStateSlot(address(GDA), account, _UNIVERSAL_INDEX_STATE_SLOT_ID, 1)[0]) << 224) >> 224 ) & 1 == 1; } diff --git a/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol index b289cace21..35f9f35642 100644 --- a/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol +++ b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol @@ -16,7 +16,6 @@ import { } from "../../contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; import { IPoolNFTBase } from "../../contracts/interfaces/agreements/gdav1/IPoolNFTBase.sol"; import { IPoolAdminNFT } from "../../contracts/interfaces/agreements/gdav1/IPoolAdminNFT.sol"; -import { IPoolMemberNFT } from "../../contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol"; import { ISuperfluidToken } from "../../contracts/interfaces/superfluid/ISuperfluidToken.sol"; import { ISETH } from "../../contracts/interfaces/tokens/ISETH.sol"; import { UUPSProxy } from "../../contracts/upgradability/UUPSProxy.sol"; @@ -1271,9 +1270,6 @@ contract FoundrySuperfluidTester is Test { ); } - // Assert Pool Member NFT is minted/burned - _assertPoolMemberNFT(poolSuperToken, pool_, member_, newUnits_); - // Assert RTB for all users // _assertRealTimeBalances(ISuperToken(address(poolSuperToken))); } @@ -1800,27 +1796,4 @@ contract FoundrySuperfluidTester is Test { { assertEq(_pool.allowance(owner, spender), expectedAllowance, "_assertPoolAllowance: allowance mismatch"); } - - function _assertPoolMemberNFT( - ISuperfluidToken _superToken, - ISuperfluidPool _pool, - address _member, - uint128 _newUnits - ) internal { - IPoolMemberNFT poolMemberNFT = SuperToken(address(_superToken)).POOL_MEMBER_NFT(); - uint256 tokenId = poolMemberNFT.getTokenId(address(_pool), address(_member)); - if (_newUnits > 0) { - // Assert Pool Member NFT owner - assertEq(poolMemberNFT.ownerOf(tokenId), _member, "_assertPoolMemberNFT: member doesn't own NFT"); - - // Assert Pool Member NFT data - IPoolMemberNFT.PoolMemberNFTData memory poolMemberData = poolMemberNFT.poolMemberDataByTokenId(tokenId); - assertEq(poolMemberData.pool, address(_pool), "_assertPoolMemberNFT: Pool Member NFT pool mismatch"); - assertEq(poolMemberData.member, _member, "_assertPoolMemberNFT: Pool Member NFT member mismatch"); - assertEq(poolMemberData.units, _newUnits, "_assertPoolMemberNFT: Pool Member NFT units mismatch"); - } else { - vm.expectRevert(IPoolNFTBase.POOL_NFT_INVALID_TOKEN_ID.selector); - poolMemberNFT.ownerOf(tokenId); - } - } } 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..598263612c 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -16,7 +16,6 @@ import { ISuperfluidToken } from "../../../../contracts/interfaces/superfluid/IS import { ISuperfluidPool, SuperfluidPool } from "../../../../contracts/agreements/gdav1/SuperfluidPool.sol"; import { IPoolNFTBase } from "../../../../contracts/interfaces/agreements/gdav1/IPoolNFTBase.sol"; import { IPoolAdminNFT } from "../../../../contracts/interfaces/agreements/gdav1/IPoolAdminNFT.sol"; -import { IPoolMemberNFT } from "../../../../contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol"; import { SuperfluidPoolStorageLayoutMock } from "./SuperfluidPoolUpgradabilityMock.t.sol"; /// @title GeneralDistributionAgreementV1 Integration Tests diff --git a/packages/ethereum-contracts/test/foundry/superfluid/PoolMemberNFT.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/PoolMemberNFT.t.sol deleted file mode 100644 index dad71ff3c9..0000000000 --- a/packages/ethereum-contracts/test/foundry/superfluid/PoolMemberNFT.t.sol +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: AGPLv3 -pragma solidity ^0.8.23; - -import { IERC165, IERC721, IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { PoolNFTBaseIntegrationTest, FakePool } from "./PoolNFTBase.t.sol"; -import { IPoolNFTBase } from "../../../contracts/interfaces/agreements/gdav1/IPoolNFTBase.sol"; -import { IPoolMemberNFT } from "../../../contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol"; -import { ISuperfluidPool } from "../../../contracts/agreements/gdav1/SuperfluidPool.sol"; -import { IGeneralDistributionAgreementV1 } from "../../../contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol"; -import "forge-std/Test.sol"; - -contract PoolMemberNFTIntegrationTest is PoolNFTBaseIntegrationTest { - /*////////////////////////////////////////////////////////////////////////// - Revert Tests - //////////////////////////////////////////////////////////////////////////*/ - - function testRevertIfTransferFromForPoolMemberNFT(address _poolAdmin, address _receiver) public { - vm.assume(_poolAdmin != address(0)); - vm.assume(_receiver != address(0)); - - ISuperfluidPool pool = sf.gda.createPool(superTokenMock, _poolAdmin, poolConfig); - uint256 nftId = _helperGetPoolMemberNftId(address(pool), alice); - - vm.startPrank(_poolAdmin); - pool.updateMemberUnits(alice, 1); - vm.stopPrank(); - - _helperRevertIfTransferFrom( - poolMemberNFT, alice, alice, _receiver, nftId, IPoolNFTBase.POOL_NFT_TRANSFER_NOT_ALLOWED.selector - ); - } - - function testRevertIfMintingForNotPool(address _pool, address _member) public { - vm.expectRevert(); - poolMemberNFT.mockMint(_pool, _member); - } - - function testRevertIfMintingForFakePool(address _admin, address _member) public { - vm.assume(_admin != address(0)); - vm.assume(_member != address(0)); - FakePool pool = new FakePool(_admin, address(superTokenMock)); - vm.expectRevert(IPoolNFTBase.POOL_NFT_NOT_REGISTERED_POOL.selector); - poolMemberNFT.mockMint(address(pool), _member); - } - - function testRevertIfMintingForZeroUnitMember() public { - address admin_ = alice; - address member = bob; - ISuperfluidPool pool = sf.gda.createPool(superTokenMock, admin_, poolConfig); - vm.expectRevert(IPoolMemberNFT.POOL_MEMBER_NFT_NO_UNITS.selector); - poolMemberNFT.mockMint(address(pool), member); - } - - function testRevertIfBurningNFTOfMemberWithUnits(address _admin, address _member) public { - vm.assume(_admin != address(0)); - vm.assume(_member != address(0)); - ISuperfluidPool pool = sf.gda.createPool(superTokenMock, _admin, poolConfig); - uint256 nftId = _helperGetPoolMemberNftId(address(pool), _member); - - vm.startPrank(_admin); - pool.updateMemberUnits(_member, 1); - vm.stopPrank(); - - vm.expectRevert(IPoolMemberNFT.POOL_MEMBER_NFT_HAS_UNITS.selector); - poolMemberNFT.mockBurn(nftId); - } - - /*////////////////////////////////////////////////////////////////////////// - Passing Tests - //////////////////////////////////////////////////////////////////////////*/ - - function testProxiableUUIDIsExpectedValue() public view { - assertEq( - poolMemberNFT.proxiableUUID(), keccak256("org.superfluid-finance.contracts.PoolMemberNFT.implementation") - ); - } - - function testTokenURIForPoolMemberNFT(uint256 tokenId) public view { - assertEq(poolMemberNFT.tokenURI(tokenId), string(abi.encodePacked(poolMemberNFT.baseURI()))); - } -} diff --git a/packages/ethereum-contracts/test/foundry/superfluid/PoolNFTBase.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/PoolNFTBase.t.sol index 03fbf57e29..826813672f 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/PoolNFTBase.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/PoolNFTBase.t.sol @@ -307,28 +307,6 @@ abstract contract PoolNFTBaseIntegrationTest is ERC721IntegrationTest { poolAdminNFT, _tokenId, _expectedAdmin, "PoolAdminNFT: owner of pool admin nft not as expected" ); } - - function _assertPoolMemberNftStateIsExpected( - uint256 _tokenId, - address _expectedPool, - address _expectedMember, - uint128 _expectedUnits - ) public view { - PoolMemberNFT.PoolMemberNFTData memory poolMemberNFTData = poolMemberNFT.poolMemberDataByTokenId(_tokenId); - - assertEq(poolMemberNFTData.pool, _expectedPool, "PoolMemberNFT: pool address not as expected"); - - // assert member is equal to expected member - assertEq(poolMemberNFTData.member, _expectedMember, "PoolMemberNFT: member address not as expected"); - - // assert units is equal to expected units - assertEq(poolMemberNFTData.units, _expectedUnits, "PoolMemberNFT: units not as expected"); - - // assert owner of pool member nft equal to expected member - _assertOwnerOfIsExpected( - poolAdminNFT, _tokenId, _expectedMember, "PoolMemberNFT: owner of pool member nft not as expected" - ); - } } /// @title PoolNFTUpgradabilityTest From 45fbbd8eb234277d9c24aa3be473ccb6771a2ce2 Mon Sep 17 00:00:00 2001 From: didi Date: Mon, 16 Jun 2025 21:22:07 +0200 Subject: [PATCH 09/14] updated changelog --- packages/ethereum-contracts/CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index 27bcfda540..d128a8db2f 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -3,11 +3,17 @@ All notable changes to the ethereum-contracts will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [unreleased] +## [v1.13.0] ### Added - `SuperToken` now implements [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) (permit extension for EIP-20 signed approvals) +- `SuperToken` now implements [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) (permit extension for EIP-20 signed approvals). +- `SuperfluidPool` now has additional methods `increaseMemberUnits` and `decreaseMemberUnits` which allow the pool admin to change member units parameterized with delta amounts. + +### Breaking +- `SuperfluidPool` does no longer mint and burn EIP-721 tokens (NFTs) on member unit updates. The gas overhead of this operation caused friction for integrations with other protocols (e.g. Uniswap V4). + ## [v1.12.1] ### Added From 5634adc48e9d7b6e325f3382f31a992fd4e337eb Mon Sep 17 00:00:00 2001 From: didi Date: Mon, 16 Jun 2025 22:02:02 +0200 Subject: [PATCH 10/14] added test --- .../agreements/gdav1/SuperfluidPool.sol | 10 ++++-- .../gdav1/GeneralDistributionAgreement.t.sol | 35 ++++++++++++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index 5e16abe53a..a504e53185 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -382,7 +382,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { /// @inheritdoc ISuperfluidPool function updateMemberUnits(address memberAddr, uint128 newUnits) external returns (bool) { - if (msg.sender != admin && msg.sender != address(GDA)) revert SUPERFLUID_POOL_NOT_POOL_ADMIN_OR_GDA(); + _enforceChangeMemberUnitsPreconditions(); uint128 oldUnits = _updateMemberUnits(memberAddr, newUnits); @@ -399,7 +399,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { /// @inheritdoc ISuperfluidPool function increaseMemberUnits(address memberAddr, uint128 addedUnits) external override returns (bool) { - if (msg.sender != admin && msg.sender != address(GDA)) revert SUPERFLUID_POOL_NOT_POOL_ADMIN_OR_GDA(); + _enforceChangeMemberUnitsPreconditions(); _updateMemberUnits(memberAddr, _getUnits(memberAddr) + addedUnits); emit Transfer(address(0), memberAddr, addedUnits); @@ -409,7 +409,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { /// @inheritdoc ISuperfluidPool function decreaseMemberUnits(address memberAddr, uint128 subtractedUnits) external override returns (bool) { - if (msg.sender != admin && msg.sender != address(GDA)) revert SUPERFLUID_POOL_NOT_POOL_ADMIN_OR_GDA(); + _enforceChangeMemberUnitsPreconditions(); _updateMemberUnits(memberAddr, _getUnits(memberAddr) - subtractedUnits); emit Transfer(memberAddr, address(0), subtractedUnits); @@ -417,6 +417,10 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { return true; } + function _enforceChangeMemberUnitsPreconditions() internal view { + if (msg.sender != admin && msg.sender != address(GDA)) revert SUPERFLUID_POOL_NOT_POOL_ADMIN_OR_GDA(); + } + function _updateMemberUnits(address memberAddr, uint128 newUnits) internal returns (uint128 oldUnits) { // @note normally we keep the sanitization in the external functions, but here // this is used in both updateMemberUnits and transfer 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 598263612c..6aca16a133 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -269,10 +269,17 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste vm.stopPrank(); } - function testRevertIfNotAdminOrGDAUpdatesMemberUnitsViaPool() public { + function testRevertIfNotAdminOrGDAChangesMemberUnitsViaPool() public { vm.startPrank(bob); vm.expectRevert(ISuperfluidPool.SUPERFLUID_POOL_NOT_POOL_ADMIN_OR_GDA.selector); freePool.updateMemberUnits(bob, 69); + + vm.expectRevert(ISuperfluidPool.SUPERFLUID_POOL_NOT_POOL_ADMIN_OR_GDA.selector); + freePool.increaseMemberUnits(bob, 69); + + vm.expectRevert(ISuperfluidPool.SUPERFLUID_POOL_NOT_POOL_ADMIN_OR_GDA.selector); + freePool.decreaseMemberUnits(bob, 69); + vm.stopPrank(); } @@ -875,6 +882,32 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste } } + function testIncreaseDecreaseMemberUnits( + address member, + uint120 increaseAmount, + uint120 decreaseAmount + ) public { + vm.assume(increaseAmount >= decreaseAmount); + + vm.startPrank(alice); + + freePool.increaseMemberUnits(member, increaseAmount); + assertEq(freePool.getUnits(member), increaseAmount); + + freePool.decreaseMemberUnits(member, decreaseAmount); + assertEq(freePool.getUnits(member), increaseAmount - decreaseAmount); + + // explicitly test for overflow and underflow behaviour + freePool.updateMemberUnits(member, 10); + vm.expectRevert(stdError.arithmeticError); + freePool.increaseMemberUnits(member, type(uint128).max); + + vm.expectRevert(stdError.arithmeticError); + freePool.decreaseMemberUnits(member, 11); + + vm.stopPrank(); + } + function testApproveAndTransferFrom( address owner, address spender, From 9c6cc979ca433fe4abd1a37b9bb7debf063c333a Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 17 Jun 2025 10:40:03 +0200 Subject: [PATCH 11/14] fix flaky test --- .../foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol | 2 ++ 1 file changed, 2 insertions(+) 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 6aca16a133..9a90aef10c 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -888,6 +888,8 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste uint120 decreaseAmount ) public { vm.assume(increaseAmount >= decreaseAmount); + vm.assume(member != address(0)); + vm.assume(member != address(freePool)); vm.startPrank(alice); From a4cee2751a9e65a74e5dc46175bfb8e9de5258d0 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 17 Jun 2025 15:27:33 +0200 Subject: [PATCH 12/14] more sensible gas price defaults for testnets (too) --- packages/ethereum-contracts/truffle-config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/ethereum-contracts/truffle-config.js b/packages/ethereum-contracts/truffle-config.js index 8829a93c28..5e247d18fd 100644 --- a/packages/ethereum-contracts/truffle-config.js +++ b/packages/ethereum-contracts/truffle-config.js @@ -221,6 +221,8 @@ const E = (module.exports = { "optimism-sepolia": { ...createNetworkDefaultConfiguration("optimism-sepolia"), network_id: 11155420, + maxPriorityFeePerGas: 1e6, // 0.001 gwei + maxFeePerGas: 1e9, // 1 gwei }, // @@ -272,6 +274,8 @@ const E = (module.exports = { "base-sepolia": { ...createNetworkDefaultConfiguration("base-sepolia"), network_id: 84532, + maxPriorityFeePerGas: 1e6, // 0.001 gwei - even 0 may do + maxFeePerGas: 1e9, // 1 gwei }, // From 35e54ee15fb4607c6633dbfc37eb7602d20c44fe Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 18 Jun 2025 16:47:29 +0200 Subject: [PATCH 13/14] convert _isPool to free function so it can be shared --- .../gdav1/GeneralDistributionAgreementV1.sol | 61 +++++++++++-------- .../agreements/gdav1/SuperfluidPool.sol | 18 +----- 2 files changed, 38 insertions(+), 41 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index c7c02135ff..c529ebc354 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -32,6 +32,27 @@ import { AgreementBase } from "../AgreementBase.sol"; import { AgreementLibrary } from "../AgreementLibrary.sol"; +/// @dev Universal Index state slot id for storing universal index data +function _universalIndexStateSlotId() pure returns (uint256) { + return 0; +} + +/// @dev returns true if the account is a pool +function _isPool( + IGeneralDistributionAgreementV1 gda, + ISuperfluidToken token, + address account +) view returns (bool exists) { + // solhint-disable var-name-mixedcase + // @note see createPool, we retrieve the isPool bit from + // UniversalIndex for this pool to determine whether the account + // is a pool + exists = ( + (uint256(token.getAgreementStateSlot(address(gda), account, _universalIndexStateSlotId(), 1)[0]) << 224) + >> 224 + ) & 1 == 1; +} + /** * @title General Distribution Agreement * @author Superfluid @@ -41,7 +62,7 @@ import { AgreementLibrary } from "../AgreementLibrary.sol"; * Agreement State * * Universal Index Data - * slotId = _UNIVERSAL_INDEX_STATE_SLOT_ID or 0 + * slotId = _universalIndexStateSlotId() or 0 * msg.sender = address of GDAv1 * account = context.msgSender * Universal Index Data stores a Basic Particle for an account as well as the total buffer and @@ -103,8 +124,6 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi address public constant SUPERFLUID_POOL_DEPLOYER_ADDRESS = address(SuperfluidPoolDeployerLibrary); - /// @dev Universal Index state slot id for storing universal index data - uint256 private constant _UNIVERSAL_INDEX_STATE_SLOT_ID = 0; /// @dev Pool member state slot id for storing subs bitmap uint256 private constant _POOL_SUBS_BITMAP_STATE_SLOT_ID = 1; /// @dev Pool member state slot id starting point for pool connections @@ -127,7 +146,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi { UniversalIndexData memory universalIndexData = _getUIndexData(abi.encode(token), account); - if (_isPool(token, account)) { + if (_isPool(this, token, account)) { rtb = ISuperfluidPool(account).getDisconnectedBalance(uint32(time)); } else { rtb = Value.unwrap(_getBasicParticleFromUIndex(universalIndexData).rtb(Time.wrap(uint32(time)))); @@ -165,7 +184,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi function getNetFlow(ISuperfluidToken token, address account) external view override returns (int96 netFlowRate) { netFlowRate = int256(FlowRate.unwrap(_getUIndex(abi.encode(token), account).flow_rate())).toInt96(); - if (_isPool(token, account)) { + if (_isPool(this, token, account)) { netFlowRate += ISuperfluidPool(account).getTotalDisconnectedFlowRate(); } @@ -274,7 +293,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi ) internal returns (ISuperfluidPool pool) { // @note ensure if token and admin are the same that nothing funky happens with echidna if (admin == address(0)) revert GDA_NO_ZERO_ADDRESS_ADMIN(); - if (_isPool(token, admin)) revert GDA_ADMIN_CANNOT_BE_POOL(); + if (_isPool(this, token, admin)) revert GDA_ADMIN_CANNOT_BE_POOL(); pool = ISuperfluidPool( address( @@ -288,7 +307,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi // to store whether an account is a pool or not bytes32[] memory data = new bytes32[](1); data[0] = bytes32(uint256(1)); - token.updateAgreementStateSlot(address(pool), _UNIVERSAL_INDEX_STATE_SLOT_ID, data); + token.updateAgreementStateSlot(address(pool), _universalIndexStateSlotId(), data); IPoolAdminNFT poolAdminNFT = IPoolAdminNFT(_getPoolAdminNFTAddress(token)); @@ -409,7 +428,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi } function appendIndexUpdateByPool(ISuperfluidToken token, BasicParticle memory p, Time t) external returns (bool) { - if (_isPool(token, msg.sender) == false) { + if (_isPool(this, token, msg.sender) == false) { revert GDA_ONLY_SUPER_TOKEN_POOL(); } bytes memory eff = abi.encode(token); @@ -422,7 +441,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi external returns (bool) { - if (_isPool(superToken, msg.sender) == false) { + if (_isPool(this, superToken, msg.sender) == false) { revert GDA_ONLY_SUPER_TOKEN_POOL(); } @@ -443,7 +462,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi newCtx = ctx; - if (_isPool(token, address(pool)) == false || + if (_isPool(this, token, address(pool)) == false || // Note: we do not support multi-tokens pools pool.superToken() != token) { revert GDA_ONLY_SUPER_TOKEN_POOL(); @@ -509,7 +528,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi int96 requestedFlowRate, bytes calldata ctx ) external override returns (bytes memory newCtx) { - if (_isPool(token, address(pool)) == false || + if (_isPool(this, token, address(pool)) == false || // Note: we do not support multi-tokens pools pool.superToken() != token) { revert GDA_ONLY_SUPER_TOKEN_POOL(); @@ -708,7 +727,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi // new buffer (universalIndexData.totalBuffer.toInt256() + Value.unwrap(bufferDelta)).toUint256(); ISuperfluidToken(token).updateAgreementStateSlot( - from, _UNIVERSAL_INDEX_STATE_SLOT_ID, _encodeUniversalIndexData(universalIndexData) + from, _universalIndexStateSlotId(), _encodeUniversalIndexData(universalIndexData) ); { @@ -837,7 +856,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi { (, universalIndexData) = _decodeUniversalIndexData( ISuperfluidToken(abi.decode(eff, (address))).getAgreementStateSlot( - address(this), owner, _UNIVERSAL_INDEX_STATE_SLOT_ID, 2 + address(this), owner, _universalIndexStateSlotId(), 2 ) ); } @@ -856,7 +875,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi function _getUIndex(bytes memory eff, address owner) internal view override returns (BasicParticle memory uIndex) { (, UniversalIndexData memory universalIndexData) = _decodeUniversalIndexData( ISuperfluidToken(abi.decode(eff, (address))).getAgreementStateSlot( - address(this), owner, _UNIVERSAL_INDEX_STATE_SLOT_ID, 2 + address(this), owner, _universalIndexStateSlotId(), 2 ) ); uIndex = _getBasicParticleFromUIndex(universalIndexData); @@ -871,7 +890,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi ISuperfluidToken(abi.decode(eff, (address))).updateAgreementStateSlot( owner, - _UNIVERSAL_INDEX_STATE_SLOT_ID, + _universalIndexStateSlotId(), _encodeUniversalIndexData(p, universalIndexData.totalBuffer, universalIndexData.isPool) ); @@ -989,17 +1008,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi /// @inheritdoc IGeneralDistributionAgreementV1 function isPool(ISuperfluidToken token, address account) external view override returns (bool) { - return _isPool(token, account); - } - - function _isPool(ISuperfluidToken token, address account) internal view returns (bool exists) { - // @note see createPool, we retrieve the isPool bit from - // UniversalIndex for this pool to determine whether the account - // is a pool - exists = ( - (uint256(token.getAgreementStateSlot(address(this), account, _UNIVERSAL_INDEX_STATE_SLOT_ID, 1)[0]) << 224) - >> 224 - ) & 1 == 1; + return _isPool(this, token, account); } // FlowDistributionData data packing: diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index a504e53185..c2df801724 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -20,7 +20,8 @@ import { import { ISuperfluid } from "../../interfaces/superfluid/ISuperfluid.sol"; import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol"; import { ISuperfluidPool } from "../../interfaces/agreements/gdav1/ISuperfluidPool.sol"; -import { GeneralDistributionAgreementV1 } from "../../agreements/gdav1/GeneralDistributionAgreementV1.sol"; +import { + GeneralDistributionAgreementV1, _isPool } from "../../agreements/gdav1/GeneralDistributionAgreementV1.sol"; import { BeaconProxiable } from "../../upgradability/BeaconProxiable.sol"; using SafeCast for uint256; @@ -424,7 +425,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { function _updateMemberUnits(address memberAddr, uint128 newUnits) internal returns (uint128 oldUnits) { // @note normally we keep the sanitization in the external functions, but here // this is used in both updateMemberUnits and transfer - if (_isPool(superToken, memberAddr)) revert SUPERFLUID_POOL_NO_POOL_MEMBERS(); + if (_isPool(GDA, superToken, memberAddr)) revert SUPERFLUID_POOL_NO_POOL_MEMBERS(); if (memberAddr == address(0)) revert SUPERFLUID_POOL_NO_ZERO_ADDRESS(); uint32 time = uint32(ISuperfluid(superToken.getHost()).getNow()); @@ -456,19 +457,6 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { emit MemberUnitsUpdated(superToken, memberAddr, oldUnits, newUnits); } - // replicates GDAv1._isPool in order to eliminate unnecessary gas cost (less external calls) - function _isPool(ISuperfluidToken token, address account) internal view returns (bool exists) { - // solhint-disable var-name-mixedcase - uint256 _UNIVERSAL_INDEX_STATE_SLOT_ID = 0; - // @note see createPool, we retrieve the isPool bit from - // UniversalIndex for this pool to determine whether the account - // is a pool - exists = ( - (uint256(token.getAgreementStateSlot(address(GDA), account, _UNIVERSAL_INDEX_STATE_SLOT_ID, 1)[0]) << 224) - >> 224 - ) & 1 == 1; - } - function _claimAll(address memberAddr, uint32 time) internal returns (int256 amount) { amount = getClaimable(memberAddr, time); assert(GDA.poolSettleClaim(superToken, memberAddr, (amount))); From b3dcea2d979f22c74fd66bf228a7ea0ad6e5d415 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 24 Jun 2025 10:52:24 +0200 Subject: [PATCH 14/14] removed duplicate line --- packages/ethereum-contracts/CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index d128a8db2f..913170e221 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -6,8 +6,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [v1.13.0] ### Added -- `SuperToken` now implements [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) (permit extension for EIP-20 signed approvals) - - `SuperToken` now implements [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) (permit extension for EIP-20 signed approvals). - `SuperfluidPool` now has additional methods `increaseMemberUnits` and `decreaseMemberUnits` which allow the pool admin to change member units parameterized with delta amounts.