From 697b97d410d0073496ce5b4524f7714afcaacf0f Mon Sep 17 00:00:00 2001 From: didi Date: Fri, 13 Jun 2025 10:32:47 +0200 Subject: [PATCH 01/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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. From 7a23d38c513b8a55fa02583f1aadf0c194726155 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 24 Jun 2025 17:44:59 +0200 Subject: [PATCH 15/25] mocks shall not use the deprecated method anymore --- .../contracts/mocks/CFAAppMocks.t.sol | 10 +++++----- .../contracts/mocks/IDASuperAppTester.t.sol | 2 +- .../contracts/mocks/MultiFlowTesterApp.t.sol | 2 +- .../contracts/mocks/StreamRedirector.t.sol | 2 +- .../contracts/mocks/SuperAppMocks.t.sol | 12 ++++++------ .../contracts/mocks/SuperTokenLibraryV1Mock.t.sol | 2 +- .../contracts/mocks/SuperfluidMock.t.sol | 7 ++++--- 7 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/ethereum-contracts/contracts/mocks/CFAAppMocks.t.sol b/packages/ethereum-contracts/contracts/mocks/CFAAppMocks.t.sol index ef7a92ce30..87c223f6d6 100644 --- a/packages/ethereum-contracts/contracts/mocks/CFAAppMocks.t.sol +++ b/packages/ethereum-contracts/contracts/mocks/CFAAppMocks.t.sol @@ -36,7 +36,7 @@ contract ExclusiveInflowTestApp is SuperAppBase { // | SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP ; - _host.registerAppWithKey(configWord, ""); + _host.registerApp(configWord); } function afterAgreementCreated( @@ -121,7 +121,7 @@ contract NonClosableOutflowTestApp is SuperAppBase { // | SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP ; - _host.registerAppWithKey(configWord, ""); + _host.registerApp(configWord); } function setupOutflow( @@ -210,7 +210,7 @@ contract SelfDeletingFlowTestApp is SuperAppBase { | SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP ; - _host.registerAppWithKey(configWord, ""); + _host.registerApp(configWord); } function afterAgreementCreated( @@ -267,7 +267,7 @@ contract ClosingOnUpdateFlowTestApp is SuperAppBase { | SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP ; - _host.registerAppWithKey(configWord, ""); + _host.registerApp(configWord); } function afterAgreementUpdated( @@ -326,7 +326,7 @@ contract FlowExchangeTestApp is SuperAppBase { | SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP ; - _host.registerAppWithKey(configWord, ""); + _host.registerApp(configWord); } function afterAgreementCreated( diff --git a/packages/ethereum-contracts/contracts/mocks/IDASuperAppTester.t.sol b/packages/ethereum-contracts/contracts/mocks/IDASuperAppTester.t.sol index ac9d2a5488..362e881929 100644 --- a/packages/ethereum-contracts/contracts/mocks/IDASuperAppTester.t.sol +++ b/packages/ethereum-contracts/contracts/mocks/IDASuperAppTester.t.sol @@ -26,7 +26,7 @@ contract IDASuperAppTester is ISuperApp { uint32 indexId) { _host = host; - _host.registerAppWithKey(configWord, ""); + _host.registerApp(configWord); _ida = ida; _token = token; _indexId = indexId; diff --git a/packages/ethereum-contracts/contracts/mocks/MultiFlowTesterApp.t.sol b/packages/ethereum-contracts/contracts/mocks/MultiFlowTesterApp.t.sol index 53476772f7..b97dc79af6 100644 --- a/packages/ethereum-contracts/contracts/mocks/MultiFlowTesterApp.t.sol +++ b/packages/ethereum-contracts/contracts/mocks/MultiFlowTesterApp.t.sol @@ -41,7 +41,7 @@ contract MultiFlowTesterApp is SuperAppBase { SuperAppDefinitions.BEFORE_AGREEMENT_CREATED_NOOP | SuperAppDefinitions.BEFORE_AGREEMENT_TERMINATED_NOOP; - _host.registerAppWithKey(configWord, ""); + _host.registerApp(configWord); } function _parseUserData( diff --git a/packages/ethereum-contracts/contracts/mocks/StreamRedirector.t.sol b/packages/ethereum-contracts/contracts/mocks/StreamRedirector.t.sol index 8f1bfd106e..6a88672e17 100644 --- a/packages/ethereum-contracts/contracts/mocks/StreamRedirector.t.sol +++ b/packages/ethereum-contracts/contracts/mocks/StreamRedirector.t.sol @@ -33,7 +33,7 @@ contract StreamRedirector is SuperAppBase { token = _token; receiver = _receiver; - host.registerAppWithKey(configWord, ""); + host.registerApp(configWord); } /** diff --git a/packages/ethereum-contracts/contracts/mocks/SuperAppMocks.t.sol b/packages/ethereum-contracts/contracts/mocks/SuperAppMocks.t.sol index 3c92cf017c..5cbf762bbe 100644 --- a/packages/ethereum-contracts/contracts/mocks/SuperAppMocks.t.sol +++ b/packages/ethereum-contracts/contracts/mocks/SuperAppMocks.t.sol @@ -52,9 +52,9 @@ contract SuperAppMock is ISuperApp { constructor(ISuperfluid host, uint256 configWord, bool doubleRegistration) { _host = host; - _host.registerAppWithKey(configWord, ""); + _host.registerApp(configWord); if (doubleRegistration) { - _host.registerAppWithKey(configWord, ""); + _host.registerApp(configWord); } _aux = new SuperAppMockAux(); } @@ -472,7 +472,7 @@ contract SuperAppMockReturningEmptyCtx { constructor(ISuperfluid host) { _host = host; - _host.registerAppWithKey(SuperAppDefinitions.APP_LEVEL_FINAL, ""); + _host.registerApp(SuperAppDefinitions.APP_LEVEL_FINAL); } function beforeAgreementCreated( @@ -533,7 +533,7 @@ contract SuperAppMockReturningInvalidCtx { constructor(ISuperfluid host) { _host = host; - _host.registerAppWithKey(SuperAppDefinitions.APP_LEVEL_FINAL, ""); + _host.registerApp(SuperAppDefinitions.APP_LEVEL_FINAL); } function afterAgreementCreated( @@ -574,7 +574,7 @@ contract SuperAppMock2ndLevel { constructor(ISuperfluid host, SuperAppMock app, AgreementMock agreement) { _host = host; - _host.registerAppWithKey(SuperAppDefinitions.APP_LEVEL_SECOND, ""); + _host.registerApp(SuperAppDefinitions.APP_LEVEL_SECOND); _app = app; _agreement = agreement; } @@ -630,6 +630,6 @@ contract SuperAppMockNotSelfRegistering { } // Factory which allows anybody to deploy arbitrary contracts as app (do NOT allow this in a real factory!) contract SuperAppFactoryMock { function registerAppWithHost(ISuperfluid host, ISuperApp app, uint256 configWord) external { - host.registerAppByFactory(app, configWord); + host.registerApp(app, configWord); } } diff --git a/packages/ethereum-contracts/contracts/mocks/SuperTokenLibraryV1Mock.t.sol b/packages/ethereum-contracts/contracts/mocks/SuperTokenLibraryV1Mock.t.sol index b41452d371..c2464582cb 100644 --- a/packages/ethereum-contracts/contracts/mocks/SuperTokenLibraryV1Mock.t.sol +++ b/packages/ethereum-contracts/contracts/mocks/SuperTokenLibraryV1Mock.t.sol @@ -384,7 +384,7 @@ contract SuperTokenLibraryCFASuperAppMock is SuperAppBase { SuperAppDefinitions.BEFORE_AGREEMENT_TERMINATED_NOOP | SuperAppDefinitions.AFTER_AGREEMENT_TERMINATED_NOOP; - host.registerAppWithKey(configWord, ""); + host.registerApp(configWord); } function createFlow(ISuperToken token) external { diff --git a/packages/ethereum-contracts/contracts/mocks/SuperfluidMock.t.sol b/packages/ethereum-contracts/contracts/mocks/SuperfluidMock.t.sol index 83206208e1..0dc11bb4a7 100644 --- a/packages/ethereum-contracts/contracts/mocks/SuperfluidMock.t.sol +++ b/packages/ethereum-contracts/contracts/mocks/SuperfluidMock.t.sol @@ -12,7 +12,7 @@ import { CallUtils } from "../libs/CallUtils.sol"; contract SuperfluidUpgradabilityTester is Superfluid { // 3_000_000 is the min callback gas limit used in a prod deployment - constructor() Superfluid(false, false, 3_000_000, address(0), address(0)) + constructor() Superfluid(false, false, 3_000_000, address(0), address(0), address(0)) // solhint-disable-next-line no-empty-blocks { } @@ -135,10 +135,11 @@ contract SuperfluidMock is Superfluid { bool appWhiteListingEnabled, uint64 callbackGasLimit, address simpleForwarderAddress, - address erc2771ForwarderAddress + address erc2771ForwarderAddress, + address allowListAddress ) Superfluid( - nonUpgradable, appWhiteListingEnabled, callbackGasLimit, simpleForwarderAddress, erc2771ForwarderAddress + nonUpgradable, appWhiteListingEnabled, callbackGasLimit, simpleForwarderAddress, erc2771ForwarderAddress, allowListAddress ) // solhint-disable-next-line no-empty-blocks { From 9d39c49bb36fa336241a596c70637b7fdd56ebce Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 24 Jun 2025 18:36:28 +0200 Subject: [PATCH 16/25] add dedicated allowlist for superapp registration --- .../interfaces/superfluid/ISuperfluid.sol | 6 +++ .../contracts/mocks/SuperfluidMock.t.sol | 3 +- .../contracts/superfluid/Superfluid.sol | 22 ++++++++- .../contracts/utils/AllowList.sol | 20 ++++++++ .../SuperfluidFrameworkDeploymentSteps.t.sol | 10 ++-- .../ops-scripts/deploy-framework.js | 47 ++++++++++++++----- .../test/foundry/superfluid/Superfluid.t.sol | 37 ++++++++++++++- packages/ethereum-contracts/truffle-config.js | 2 +- 8 files changed, 126 insertions(+), 21 deletions(-) create mode 100644 packages/ethereum-contracts/contracts/utils/AllowList.sol diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol index 97d60cc6e3..d0dbd9a039 100644 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol +++ b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol @@ -658,6 +658,12 @@ interface ISuperfluid { // solhint-disable func-name-mixedcase function getERC2771Forwarder() external view returns(address); + /** + * @dev returns the address of the allowlist contract used for permissioning superapp registration. + * @return address of the allowlist contract + */ + function getSuperAppRegistrationAllowlist() external view returns(address); + /************************************************************************** * Function modifiers for access control and parameter validations * diff --git a/packages/ethereum-contracts/contracts/mocks/SuperfluidMock.t.sol b/packages/ethereum-contracts/contracts/mocks/SuperfluidMock.t.sol index 0dc11bb4a7..606d2d5f89 100644 --- a/packages/ethereum-contracts/contracts/mocks/SuperfluidMock.t.sol +++ b/packages/ethereum-contracts/contracts/mocks/SuperfluidMock.t.sol @@ -139,7 +139,8 @@ contract SuperfluidMock is Superfluid { address allowListAddress ) Superfluid( - nonUpgradable, appWhiteListingEnabled, callbackGasLimit, simpleForwarderAddress, erc2771ForwarderAddress, allowListAddress + nonUpgradable, appWhiteListingEnabled, callbackGasLimit, simpleForwarderAddress, erc2771ForwarderAddress, + allowListAddress ) // solhint-disable-next-line no-empty-blocks { diff --git a/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol b/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol index 258a1f4856..588d4e971b 100644 --- a/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol +++ b/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol @@ -25,6 +25,7 @@ import { CallbackUtils } from "../libs/CallbackUtils.sol"; import { BaseRelayRecipient } from "../libs/BaseRelayRecipient.sol"; import { SimpleForwarder } from "../utils/SimpleForwarder.sol"; import { ERC2771Forwarder } from "../utils/ERC2771Forwarder.sol"; +import { AllowList } from "../utils/AllowList.sol"; /** * @dev The Superfluid host implementation. @@ -59,6 +60,9 @@ contract Superfluid is SimpleForwarder immutable public SIMPLE_FORWARDER; ERC2771Forwarder immutable internal _ERC2771_FORWARDER; + // Allowlist for superapp registration + AllowList immutable internal _SUPERAPP_REGISTRATION_ALLOWLIST; + /** * @dev Maximum number of level of apps can be composed together * @@ -105,13 +109,15 @@ contract Superfluid is bool appWhiteListingEnabled, uint64 callbackGasLimit, address simpleForwarderAddress, - address erc2771ForwarderAddress + address erc2771ForwarderAddress, + address superappRegistrationAllowlistAddress ) { NON_UPGRADABLE_DEPLOYMENT = nonUpgradable; APP_WHITE_LISTING_ENABLED = appWhiteListingEnabled; CALLBACK_GAS_LIMIT = callbackGasLimit; SIMPLE_FORWARDER = SimpleForwarder(simpleForwarderAddress); _ERC2771_FORWARDER = ERC2771Forwarder(erc2771ForwarderAddress); + _SUPERAPP_REGISTRATION_ALLOWLIST = AllowList(superappRegistrationAllowlistAddress); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -375,8 +381,16 @@ contract Superfluid is _registerApp(ISuperApp(msg.sender), configWord); } - // internally we keep using the gov config method with key + // Checks if the deployer account has permission to register SuperApps, reverts if not. + // New method: lookup in the dedicated allowlist contract. + // Legacy/fallback method: lookup in the governance contract. function _enforceAppRegistrationPermissioning(string memory registrationKey, address deployer) internal view { + // new method: check if the deployer is in the allowlist + if (_SUPERAPP_REGISTRATION_ALLOWLIST.hasPermission(deployer)) { + return; + } + + // legacy/fallback method: check if permission is given by gov bytes32 configKey = SuperfluidGovernanceConfigs.getAppRegistrationConfigKey( // solhint-disable-next-line avoid-tx-origin deployer, @@ -958,6 +972,10 @@ contract Superfluid is return address(_ERC2771_FORWARDER); } + function getSuperAppRegistrationAllowlist() external view override returns(address) { + return address(_SUPERAPP_REGISTRATION_ALLOWLIST); + } + /************************************************************************** * Internal **************************************************************************/ diff --git a/packages/ethereum-contracts/contracts/utils/AllowList.sol b/packages/ethereum-contracts/contracts/utils/AllowList.sol new file mode 100644 index 0000000000..5cf1e43f27 --- /dev/null +++ b/packages/ethereum-contracts/contracts/utils/AllowList.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +contract AllowList is Ownable { + mapping(address => bool) internal _map; + + function givePermission(address account) external onlyOwner { + _map[account] = true; + } + + function revokePermission(address account) external onlyOwner { + _map[account] = false; + } + + function hasPermission(address account) external view returns (bool) { + return _map[account]; + } +} \ No newline at end of file diff --git a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol index 4a7052e69e..8b44568ac2 100644 --- a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol +++ b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol @@ -31,6 +31,7 @@ import { TOGA } from "./TOGA.sol"; import { IResolver } from "../interfaces/utils/IResolver.sol"; import { SimpleForwarder } from "../utils/SimpleForwarder.sol"; import { ERC2771Forwarder } from "../utils/ERC2771Forwarder.sol"; +import { AllowList } from "../utils/AllowList.sol"; import { MacroForwarder } from "../utils/MacroForwarder.sol"; /// @title Superfluid Framework Deployment Steps @@ -143,10 +144,11 @@ contract SuperfluidFrameworkDeploymentSteps { } else if (step == 1) { // CORE CONTRACT: Superfluid (Host) SimpleForwarder simpleForwarder = new SimpleForwarder(); ERC2771Forwarder erc2771Forwarder = new ERC2771Forwarder(); + AllowList allowList = new AllowList(); // Deploy Host and initialize the test governance. // 3_000_000 is the min callback gas limit used in a prod deployment host = SuperfluidHostDeployerLibrary.deploy( - true, false, 3_000_000, address(simpleForwarder), address(erc2771Forwarder) + true, false, 3_000_000, address(simpleForwarder), address(erc2771Forwarder), address(allowList) ); simpleForwarder.transferOwnership(address(host)); erc2771Forwarder.transferOwnership(address(host)); @@ -319,12 +321,14 @@ library SuperfluidHostDeployerLibrary { bool _appWhiteListingEnabled, uint64 callbackGasLimit, address simpleForwarderAddress, - address erc2771ForwarderAddress + address erc2771ForwarderAddress, + address allowListAddress ) external returns (Superfluid) { return new Superfluid( - _nonUpgradable, _appWhiteListingEnabled, callbackGasLimit, simpleForwarderAddress, erc2771ForwarderAddress + _nonUpgradable, _appWhiteListingEnabled, callbackGasLimit, simpleForwarderAddress, erc2771ForwarderAddress, + allowListAddress ); } } diff --git a/packages/ethereum-contracts/ops-scripts/deploy-framework.js b/packages/ethereum-contracts/ops-scripts/deploy-framework.js index 2c6709af45..08c4ef7b18 100644 --- a/packages/ethereum-contracts/ops-scripts/deploy-framework.js +++ b/packages/ethereum-contracts/ops-scripts/deploy-framework.js @@ -234,6 +234,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( "IAccessControlEnumerable", "SimpleForwarder", "ERC2771Forwarder", + "AllowList", ]; const mockContracts = [ "SuperfluidMock", @@ -273,6 +274,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( IAccessControlEnumerable, SimpleForwarder, ERC2771Forwarder, + AllowList, } = await SuperfluidSDK.loadContracts({ ...extractWeb3Options(options), additionalContracts: contracts.concat(useMocks ? mockContracts : []), @@ -361,11 +363,15 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( console.log("ERC2771Forwarder address:", erc2771Forwarder.address); output += `ERC2771_FORWARDER=${erc2771Forwarder.address}\n`; + const allowList = await web3tx(AllowList.new, "AllowList.new")(); + console.log("AllowList address:", allowList.address); + output += `SUPERAPP_REGISTRATION_ALLOWLIST=${allowList.address}\n`; + let superfluidAddress; const superfluidLogic = await web3tx( SuperfluidLogic.new, "SuperfluidLogic.new" - )(nonUpgradable, appWhiteListing, appCallbackGasLimit, simpleForwarder.address, erc2771Forwarder.address); + )(nonUpgradable, appWhiteListing, appCallbackGasLimit, simpleForwarder.address, erc2771Forwarder.address, allowList.address); console.log( `Superfluid new code address ${superfluidLogic.address}` ); @@ -803,17 +809,6 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( return ZERO_ADDRESS; }); - if (prevAddr !== ZERO_ADDRESS) { - // TEMPORARY FIX - can be removed after applied - // we found a previous deployment. Now verify it has the host as owner. - // the first mainnet deployment didn't have this for SimpleForwarder, thus needs a redeployment. - const ownerAddr = await (await Ownable.at(prevAddr)).owner(); - if (ownerAddr != superfluid.address) { - console.log(` !!! ${outputKey} has wrong owner, needs re-deployment`); - prevAddr = ZERO_ADDRESS; // by setting zero, we force a re-deployment - } - } - const newAddress = await deployContractIfCodeChanged( web3, ForwarderContract, @@ -833,6 +828,25 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( return newAddress !== ZERO_ADDRESS ? newAddress : prevAddr; } + async function getOrDeployHelper( + Contract, + getPrevAddrFn, + outputKey + ) { + let prevAddr = await getPrevAddrFn().catch(_err => { + console.error(`### Error getting ${Contract.contractName} address, likely not yet deployed`); + return ZERO_ADDRESS; + }); + + if (prevAddr !== ZERO_ADDRESS) { + return prevAddr; + } + + const instance = await web3tx(Contract.new, `${Contract.contractName}.new`)(); + output += `${outputKey}=${instance.address}\n`; + return instance.address; + } + const simpleForwarderAddress = await getOrDeployForwarder( superfluid, SimpleForwarder, @@ -847,6 +861,12 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( "ERC2771_FORWARDER" ); + const allowListAddress = await getOrDeployHelper( + AllowList, + () => superfluid.getSuperAppRegistrationAllowlist(), + "SUPERAPP_REGISTRATION_ALLOWLIST" + ); + // get previous callback gas limit, make sure we don't decrease it const prevCallbackGasLimit = await superfluid.CALLBACK_GAS_LIMIT(); if (prevCallbackGasLimit.toNumber() > appCallbackGasLimit) { @@ -867,13 +887,14 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( const superfluidLogic = await web3tx( SuperfluidLogic.new, "SuperfluidLogic.new" - )(nonUpgradable, appWhiteListing, appCallbackGasLimit, simpleForwarderAddress, erc2771ForwarderAddress); + )(nonUpgradable, appWhiteListing, appCallbackGasLimit, simpleForwarderAddress, erc2771ForwarderAddress, allowListAddress); output += `SUPERFLUID_HOST_LOGIC=${superfluidLogic.address}\n`; return superfluidLogic.address; }, [ ap(erc2771ForwarderAddress), ap(simpleForwarderAddress), + ap(allowListAddress), appCallbackGasLimit.toString(16).padStart(64, "0") ], ); diff --git a/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol index e36ac75b84..1af5085b14 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol @@ -6,8 +6,10 @@ import { UUPSProxiable } from "../../../contracts/upgradability/UUPSProxiable.so import { SuperToken } from "../../../contracts/superfluid/SuperToken.sol"; import { SuperTokenV1Library } from "../../../contracts/apps/SuperTokenV1Library.sol"; import { ISuperAgreement } from "../../../contracts/interfaces/superfluid/ISuperAgreement.sol"; -import { ISuperfluid } from "../../../contracts/interfaces/superfluid/ISuperfluid.sol"; +import { ISuperfluid, SuperAppDefinitions } from "../../../contracts/interfaces/superfluid/ISuperfluid.sol"; +import { ISuperApp } from "../../../contracts/interfaces/superfluid/ISuperApp.sol"; import { AgreementMock } from "../../../contracts/mocks/AgreementMock.t.sol"; +import { AllowList } from "../../../contracts/utils/AllowList.sol"; contract SuperfluidIntegrationTest is FoundrySuperfluidTester { using SuperTokenV1Library for SuperToken; @@ -83,4 +85,37 @@ contract SuperfluidIntegrationTest is FoundrySuperfluidTester { sf.host.changeSuperTokenAdmin(superToken, newAdmin); vm.stopPrank(); } + + function testSuperAppRegistrationViaAllowList() public { + AllowList allowList = new AllowList(); + Superfluid hostWithAllowList = new Superfluid( + true, true, 3_000_000, address(0), address(0), address(allowList) + ); + hostWithAllowList.initialize(sf.governance); + + // first, give permission to alice + address allowlistAddress = address(hostWithAllowList.getSuperAppRegistrationAllowlist()); + // get allowlist owner + address allowListOwner = AllowList(allowlistAddress).owner(); + + // give permission to alice + vm.startPrank(allowListOwner); + AllowList(allowlistAddress).givePermission(alice); + vm.stopPrank(); + + // any address which is a contract is ok for the purpose of this test + ISuperApp mockSuperApp = ISuperApp(address(this)); + + // as bob, try to register a superapp - should revert + vm.startPrank(bob); + vm.expectRevert(ISuperfluid.HOST_NO_APP_REGISTRATION_PERMISSION.selector); + hostWithAllowList.registerApp(mockSuperApp, SuperAppDefinitions.APP_LEVEL_FINAL); + vm.stopPrank(); + + // as alice, try to register a superapp - should succeed + vm.startPrank(alice); + hostWithAllowList.registerApp(mockSuperApp, SuperAppDefinitions.APP_LEVEL_FINAL); + vm.stopPrank(); + vm.assertTrue(hostWithAllowList.isApp(mockSuperApp)); + } } diff --git a/packages/ethereum-contracts/truffle-config.js b/packages/ethereum-contracts/truffle-config.js index 5e247d18fd..d89b4a0e67 100644 --- a/packages/ethereum-contracts/truffle-config.js +++ b/packages/ethereum-contracts/truffle-config.js @@ -275,7 +275,7 @@ const E = (module.exports = { ...createNetworkDefaultConfiguration("base-sepolia"), network_id: 84532, maxPriorityFeePerGas: 1e6, // 0.001 gwei - even 0 may do - maxFeePerGas: 1e9, // 1 gwei + maxFeePerGas: 1e8, // 0.1 gwei }, // From 959e9d363c5735f59f4db4131e1b4d819a388e19 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 24 Jun 2025 18:59:21 +0200 Subject: [PATCH 17/25] test revoke too --- .../test/foundry/superfluid/Superfluid.t.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol index 1af5085b14..4830009dae 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol @@ -117,5 +117,16 @@ contract SuperfluidIntegrationTest is FoundrySuperfluidTester { hostWithAllowList.registerApp(mockSuperApp, SuperAppDefinitions.APP_LEVEL_FINAL); vm.stopPrank(); vm.assertTrue(hostWithAllowList.isApp(mockSuperApp)); + + // revoke permission from alice + vm.startPrank(allowListOwner); + AllowList(allowlistAddress).revokePermission(alice); + vm.stopPrank(); + + // as alice, try to register a superapp - should revert + vm.startPrank(alice); + vm.expectRevert(ISuperfluid.HOST_NO_APP_REGISTRATION_PERMISSION.selector); + hostWithAllowList.registerApp(mockSuperApp, SuperAppDefinitions.APP_LEVEL_FINAL); + vm.stopPrank(); } } From a2f295cce62e9704262918da67ce377705633c82 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 1 Jul 2025 14:59:38 +0200 Subject: [PATCH 18/25] use OZ AccessControl instead of a custom implementation --- .../interfaces/superfluid/ISuperfluid.sol | 6 +- .../contracts/superfluid/Superfluid.sol | 22 +++--- .../contracts/utils/ACL.sol | 16 ++++ .../contracts/utils/AllowList.sol | 20 ----- .../SuperfluidFrameworkDeploymentSteps.t.sol | 6 +- .../ops-scripts/deploy-framework.js | 24 +++--- .../tasks/bundled-abi-contracts-list.json | 3 +- .../test/foundry/superfluid/Superfluid.t.sol | 78 ++++++++++++------- 8 files changed, 99 insertions(+), 76 deletions(-) create mode 100644 packages/ethereum-contracts/contracts/utils/ACL.sol delete mode 100644 packages/ethereum-contracts/contracts/utils/AllowList.sol diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol index d0dbd9a039..a23544e684 100644 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol +++ b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol @@ -659,10 +659,10 @@ interface ISuperfluid { function getERC2771Forwarder() external view returns(address); /** - * @dev returns the address of the allowlist contract used for permissioning superapp registration. - * @return address of the allowlist contract + * @dev returns the address of the ACL contract used for granular permissioning. + * @return address of the ACL contract */ - function getSuperAppRegistrationAllowlist() external view returns(address); + function getACL() external view returns(address); /************************************************************************** * Function modifiers for access control and parameter validations diff --git a/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol b/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol index 588d4e971b..3e3bd3bc99 100644 --- a/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol +++ b/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol @@ -25,7 +25,7 @@ import { CallbackUtils } from "../libs/CallbackUtils.sol"; import { BaseRelayRecipient } from "../libs/BaseRelayRecipient.sol"; import { SimpleForwarder } from "../utils/SimpleForwarder.sol"; import { ERC2771Forwarder } from "../utils/ERC2771Forwarder.sol"; -import { AllowList } from "../utils/AllowList.sol"; +import { ACL } from "../utils/ACL.sol"; /** * @dev The Superfluid host implementation. @@ -60,8 +60,8 @@ contract Superfluid is SimpleForwarder immutable public SIMPLE_FORWARDER; ERC2771Forwarder immutable internal _ERC2771_FORWARDER; - // Allowlist for superapp registration - AllowList immutable internal _SUPERAPP_REGISTRATION_ALLOWLIST; + // ACL (for superapp registration) + ACL immutable internal _ACL; /** * @dev Maximum number of level of apps can be composed together @@ -75,6 +75,8 @@ contract Superfluid is uint32 constant public MAX_NUM_AGREEMENTS = 256; + bytes32 constant public ACL_SUPERAPP_REGISTRATION_ROLE = keccak256("ACL_SUPERAPP_REGISTRATION_ROLE"); + /* WARNING: NEVER RE-ORDER VARIABLES! Always double-check that new variables are added APPEND-ONLY. Re-ordering variables can permanently BREAK the deployed proxy contract. */ @@ -110,14 +112,14 @@ contract Superfluid is uint64 callbackGasLimit, address simpleForwarderAddress, address erc2771ForwarderAddress, - address superappRegistrationAllowlistAddress + address aclAddress ) { NON_UPGRADABLE_DEPLOYMENT = nonUpgradable; APP_WHITE_LISTING_ENABLED = appWhiteListingEnabled; CALLBACK_GAS_LIMIT = callbackGasLimit; SIMPLE_FORWARDER = SimpleForwarder(simpleForwarderAddress); _ERC2771_FORWARDER = ERC2771Forwarder(erc2771ForwarderAddress); - _SUPERAPP_REGISTRATION_ALLOWLIST = AllowList(superappRegistrationAllowlistAddress); + _ACL = ACL(aclAddress); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -382,11 +384,11 @@ contract Superfluid is } // Checks if the deployer account has permission to register SuperApps, reverts if not. - // New method: lookup in the dedicated allowlist contract. + // New method: lookup in the ACL contract. // Legacy/fallback method: lookup in the governance contract. function _enforceAppRegistrationPermissioning(string memory registrationKey, address deployer) internal view { - // new method: check if the deployer is in the allowlist - if (_SUPERAPP_REGISTRATION_ALLOWLIST.hasPermission(deployer)) { + // new method: check if the deployer is granted permission in the ACL + if (_ACL.hasRole(ACL_SUPERAPP_REGISTRATION_ROLE, deployer)) { return; } @@ -972,8 +974,8 @@ contract Superfluid is return address(_ERC2771_FORWARDER); } - function getSuperAppRegistrationAllowlist() external view override returns(address) { - return address(_SUPERAPP_REGISTRATION_ALLOWLIST); + function getACL() external view override returns(address) { + return address(_ACL); } /************************************************************************** diff --git a/packages/ethereum-contracts/contracts/utils/ACL.sol b/packages/ethereum-contracts/contracts/utils/ACL.sol new file mode 100644 index 0000000000..12c4bc7306 --- /dev/null +++ b/packages/ethereum-contracts/contracts/utils/ACL.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; + +contract ACL is AccessControl { + constructor() { + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + /// Allows the default admin to set the admin role for a given role. + /// This gives the flexibility to set up sophisticated permissioning schemes in the future. + function setRoleAdmin(bytes32 role, bytes32 adminRole) external onlyRole(DEFAULT_ADMIN_ROLE) { + _setRoleAdmin(role, adminRole); + } +} \ No newline at end of file diff --git a/packages/ethereum-contracts/contracts/utils/AllowList.sol b/packages/ethereum-contracts/contracts/utils/AllowList.sol deleted file mode 100644 index 5cf1e43f27..0000000000 --- a/packages/ethereum-contracts/contracts/utils/AllowList.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; - -contract AllowList is Ownable { - mapping(address => bool) internal _map; - - function givePermission(address account) external onlyOwner { - _map[account] = true; - } - - function revokePermission(address account) external onlyOwner { - _map[account] = false; - } - - function hasPermission(address account) external view returns (bool) { - return _map[account]; - } -} \ No newline at end of file diff --git a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol index 8b44568ac2..aad3be19ce 100644 --- a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol +++ b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol @@ -31,7 +31,7 @@ import { TOGA } from "./TOGA.sol"; import { IResolver } from "../interfaces/utils/IResolver.sol"; import { SimpleForwarder } from "../utils/SimpleForwarder.sol"; import { ERC2771Forwarder } from "../utils/ERC2771Forwarder.sol"; -import { AllowList } from "../utils/AllowList.sol"; +import { ACL } from "../utils/ACL.sol"; import { MacroForwarder } from "../utils/MacroForwarder.sol"; /// @title Superfluid Framework Deployment Steps @@ -144,11 +144,11 @@ contract SuperfluidFrameworkDeploymentSteps { } else if (step == 1) { // CORE CONTRACT: Superfluid (Host) SimpleForwarder simpleForwarder = new SimpleForwarder(); ERC2771Forwarder erc2771Forwarder = new ERC2771Forwarder(); - AllowList allowList = new AllowList(); + ACL acl = new ACL(); // Deploy Host and initialize the test governance. // 3_000_000 is the min callback gas limit used in a prod deployment host = SuperfluidHostDeployerLibrary.deploy( - true, false, 3_000_000, address(simpleForwarder), address(erc2771Forwarder), address(allowList) + true, false, 3_000_000, address(simpleForwarder), address(erc2771Forwarder), address(acl) ); simpleForwarder.transferOwnership(address(host)); erc2771Forwarder.transferOwnership(address(host)); diff --git a/packages/ethereum-contracts/ops-scripts/deploy-framework.js b/packages/ethereum-contracts/ops-scripts/deploy-framework.js index 08c4ef7b18..df104242a9 100644 --- a/packages/ethereum-contracts/ops-scripts/deploy-framework.js +++ b/packages/ethereum-contracts/ops-scripts/deploy-framework.js @@ -234,7 +234,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( "IAccessControlEnumerable", "SimpleForwarder", "ERC2771Forwarder", - "AllowList", + "ACL", ]; const mockContracts = [ "SuperfluidMock", @@ -274,7 +274,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( IAccessControlEnumerable, SimpleForwarder, ERC2771Forwarder, - AllowList, + ACL, } = await SuperfluidSDK.loadContracts({ ...extractWeb3Options(options), additionalContracts: contracts.concat(useMocks ? mockContracts : []), @@ -363,15 +363,15 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( console.log("ERC2771Forwarder address:", erc2771Forwarder.address); output += `ERC2771_FORWARDER=${erc2771Forwarder.address}\n`; - const allowList = await web3tx(AllowList.new, "AllowList.new")(); - console.log("AllowList address:", allowList.address); - output += `SUPERAPP_REGISTRATION_ALLOWLIST=${allowList.address}\n`; + const acl = await web3tx(ACL.new, "ACL.new")(); + console.log("ACL address:", acl.address); + output += `ACL=${acl.address}\n`; let superfluidAddress; const superfluidLogic = await web3tx( SuperfluidLogic.new, "SuperfluidLogic.new" - )(nonUpgradable, appWhiteListing, appCallbackGasLimit, simpleForwarder.address, erc2771Forwarder.address, allowList.address); + )(nonUpgradable, appWhiteListing, appCallbackGasLimit, simpleForwarder.address, erc2771Forwarder.address, acl.address); console.log( `Superfluid new code address ${superfluidLogic.address}` ); @@ -861,10 +861,10 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( "ERC2771_FORWARDER" ); - const allowListAddress = await getOrDeployHelper( - AllowList, - () => superfluid.getSuperAppRegistrationAllowlist(), - "SUPERAPP_REGISTRATION_ALLOWLIST" + const aclAddress = await getOrDeployHelper( + ACL, + () => superfluid.getACL(), + "ACL" ); // get previous callback gas limit, make sure we don't decrease it @@ -887,14 +887,14 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( const superfluidLogic = await web3tx( SuperfluidLogic.new, "SuperfluidLogic.new" - )(nonUpgradable, appWhiteListing, appCallbackGasLimit, simpleForwarderAddress, erc2771ForwarderAddress, allowListAddress); + )(nonUpgradable, appWhiteListing, appCallbackGasLimit, simpleForwarderAddress, erc2771ForwarderAddress, aclAddress); output += `SUPERFLUID_HOST_LOGIC=${superfluidLogic.address}\n`; return superfluidLogic.address; }, [ ap(erc2771ForwarderAddress), ap(simpleForwarderAddress), - ap(allowListAddress), + ap(aclAddress), appCallbackGasLimit.toString(16).padStart(64, "0") ], ); diff --git a/packages/ethereum-contracts/tasks/bundled-abi-contracts-list.json b/packages/ethereum-contracts/tasks/bundled-abi-contracts-list.json index dcb99646a4..40d1eae34f 100644 --- a/packages/ethereum-contracts/tasks/bundled-abi-contracts-list.json +++ b/packages/ethereum-contracts/tasks/bundled-abi-contracts-list.json @@ -21,5 +21,6 @@ "IPureSuperToken", "IResolver", "Resolver", "TestResolver", "SuperfluidLoader", - "IUserDefinedMacro" + "IUserDefinedMacro", + "ACL" ] diff --git a/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol index 4830009dae..eb2e1fd7b6 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol @@ -9,7 +9,8 @@ import { ISuperAgreement } from "../../../contracts/interfaces/superfluid/ISuper import { ISuperfluid, SuperAppDefinitions } from "../../../contracts/interfaces/superfluid/ISuperfluid.sol"; import { ISuperApp } from "../../../contracts/interfaces/superfluid/ISuperApp.sol"; import { AgreementMock } from "../../../contracts/mocks/AgreementMock.t.sol"; -import { AllowList } from "../../../contracts/utils/AllowList.sol"; +import { SuperAppMockNotSelfRegistering } from "../../../contracts/mocks/SuperAppMocks.t.sol"; +import { ACL } from "../../../contracts/utils/ACL.sol"; contract SuperfluidIntegrationTest is FoundrySuperfluidTester { using SuperTokenV1Library for SuperToken; @@ -33,7 +34,9 @@ contract SuperfluidIntegrationTest is FoundrySuperfluidTester { vm.startPrank(sf.governance.owner()); sf.governance.registerAgreementClass(sf.host, address(agreementMock)); vm.stopPrank(); - agreementMock = sf.host.NON_UPGRADABLE_DEPLOYMENT() ? agreementMock : AgreementMock(address(sf.host.getAgreementClass(id))); + agreementMock = sf.host.NON_UPGRADABLE_DEPLOYMENT() + ? agreementMock + : AgreementMock(address(sf.host.getAgreementClass(id))); mocks[i + _NUM_AGREEMENTS] = ISuperAgreement(address(agreementMock)); } @@ -86,47 +89,68 @@ contract SuperfluidIntegrationTest is FoundrySuperfluidTester { vm.stopPrank(); } - function testSuperAppRegistrationViaAllowList() public { - AllowList allowList = new AllowList(); - Superfluid hostWithAllowList = new Superfluid( - true, true, 3_000_000, address(0), address(0), address(allowList) + function testSuperAppRegistrationViaACL() public { + ACL acl = new ACL(); + Superfluid hostWithACL = new Superfluid( + true, true, 3_000_000, address(0), address(0), address(acl) ); - hostWithAllowList.initialize(sf.governance); + ISuperApp mockSuperApp1 = ISuperApp(address(new SuperAppMockNotSelfRegistering())); + ISuperApp mockSuperApp2 = ISuperApp(address(new SuperAppMockNotSelfRegistering())); - // first, give permission to alice - address allowlistAddress = address(hostWithAllowList.getSuperAppRegistrationAllowlist()); - // get allowlist owner - address allowListOwner = AllowList(allowlistAddress).owner(); + hostWithACL.initialize(sf.governance); - // give permission to alice - vm.startPrank(allowListOwner); - AllowList(allowlistAddress).givePermission(alice); - vm.stopPrank(); + bytes32 aclSuperAppRegRole = hostWithACL.ACL_SUPERAPP_REGISTRATION_ROLE(); + bytes32 aclAdminRole = acl.DEFAULT_ADMIN_ROLE(); + + // first, give permission to alice + address aclAddress = address(hostWithACL.getACL()); - // any address which is a contract is ok for the purpose of this test - ISuperApp mockSuperApp = ISuperApp(address(this)); + acl.grantRole(aclSuperAppRegRole, alice); // as bob, try to register a superapp - should revert vm.startPrank(bob); - vm.expectRevert(ISuperfluid.HOST_NO_APP_REGISTRATION_PERMISSION.selector); - hostWithAllowList.registerApp(mockSuperApp, SuperAppDefinitions.APP_LEVEL_FINAL); - vm.stopPrank(); + vm.expectRevert(); + hostWithACL.registerApp(mockSuperApp1, SuperAppDefinitions.APP_LEVEL_FINAL); // as alice, try to register a superapp - should succeed vm.startPrank(alice); - hostWithAllowList.registerApp(mockSuperApp, SuperAppDefinitions.APP_LEVEL_FINAL); + hostWithACL.registerApp(mockSuperApp1, SuperAppDefinitions.APP_LEVEL_FINAL); vm.stopPrank(); - vm.assertTrue(hostWithAllowList.isApp(mockSuperApp)); + vm.assertTrue(hostWithACL.isApp(mockSuperApp1)); // revoke permission from alice - vm.startPrank(allowListOwner); - AllowList(allowlistAddress).revokePermission(alice); - vm.stopPrank(); + acl.revokeRole(aclSuperAppRegRole, alice); // as alice, try to register a superapp - should revert vm.startPrank(alice); - vm.expectRevert(ISuperfluid.HOST_NO_APP_REGISTRATION_PERMISSION.selector); - hostWithAllowList.registerApp(mockSuperApp, SuperAppDefinitions.APP_LEVEL_FINAL); + vm.expectRevert(); + hostWithACL.registerApp(mockSuperApp2, SuperAppDefinitions.APP_LEVEL_FINAL); + vm.stopPrank(); + + // nobody else can grant permission to register superapps + vm.startPrank(eve); + vm.expectRevert(); + acl.grantRole(aclSuperAppRegRole, bob); + vm.stopPrank(); + + // nobody can define admin roles ... + bytes32 dedicatedAdminRole = keccak256("SUPER_APP_REGISTRATION_ADMIN"); + vm.startPrank(eve); + vm.expectRevert(); + acl.setRoleAdmin(aclSuperAppRegRole, dedicatedAdminRole); + vm.stopPrank(); + + // ... except the default admin + // (This is to be done in a possible future where we start using this ACL for other purposes too + // and want to have a more sophisticated permissioning scheme.) + acl.setRoleAdmin(aclSuperAppRegRole, dedicatedAdminRole); + + // grant heidi the new admin role + acl.grantRole(dedicatedAdminRole, heidi); + + // now heidi can manage permissions for superapp deployers + vm.startPrank(heidi); + acl.grantRole(aclSuperAppRegRole, bob); vm.stopPrank(); } } From ce53cea38445f38a8cccce07066436286126be21 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 1 Jul 2025 15:03:30 +0200 Subject: [PATCH 19/25] 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 913170e221..78dc039b06 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -9,6 +9,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `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. +### Changed +- Curation of the SuperApp registration allowlist can now be delegated by governance to a newly added `ACL` contract. + ### 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). From fbf3ad40ca07e3cfd9295e94daa95dc04e79f15c Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 1 Jul 2025 17:16:07 +0200 Subject: [PATCH 20/25] rename to SimpleACL --- .../interfaces/superfluid/ISuperfluid.sol | 18 ++++++--- .../contracts/superfluid/Superfluid.sol | 16 ++++---- .../utils/{ACL.sol => SimpleACL.sol} | 2 +- .../SuperfluidFrameworkDeploymentSteps.t.sol | 6 +-- .../ops-scripts/deploy-framework.js | 24 +++++------ .../tasks/bundled-abi-contracts-list.json | 2 +- .../test/foundry/superfluid/Superfluid.t.sol | 40 +++++++++---------- 7 files changed, 58 insertions(+), 50 deletions(-) rename packages/ethereum-contracts/contracts/utils/{ACL.sol => SimpleACL.sol} (93%) diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol index a23544e684..7a43da35c2 100644 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol +++ b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol @@ -658,11 +658,19 @@ interface ISuperfluid { // solhint-disable func-name-mixedcase function getERC2771Forwarder() external view returns(address); - /** - * @dev returns the address of the ACL contract used for granular permissioning. - * @return address of the ACL contract - */ - function getACL() external view returns(address); + // solhint-disable max-line-length + /** + * @dev returns the address of the SimpleACL contract (currently used for SuperApp registration permissioning). + * That contract implements the interface [IAccessControl](https://docs.openzeppelin.com/contracts/4.x/api/access#IAccessControl), + * which provides the following functions: + * - [hasRole(role, account)](https://docs.openzeppelin.com/contracts/4.x/api/access#IAccessControl-hasRole-bytes32-address-) + * - [getRoleAdmin(role)](https://docs.openzeppelin.com/contracts/4.x/api/access#IAccessControl-getRoleAdmin-bytes32-) + * - [grantRole(role, account)](https://docs.openzeppelin.com/contracts/4.x/api/access#IAccessControl-grantRole-bytes32-address-) + * - [revokeRole(role, account)](https://docs.openzeppelin.com/contracts/4.x/api/access#IAccessControl-revokeRole-bytes32-address-) + * - [renounceRole(role, account)](https://docs.openzeppelin.com/contracts/4.x/api/access#IAccessControl-renounceRole-bytes32-address-) + * @return address of the SimpleACL contract + */ + function getSimpleACL() external view returns(address); /************************************************************************** * Function modifiers for access control and parameter validations diff --git a/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol b/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol index 3e3bd3bc99..53c995c21e 100644 --- a/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol +++ b/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol @@ -25,7 +25,7 @@ import { CallbackUtils } from "../libs/CallbackUtils.sol"; import { BaseRelayRecipient } from "../libs/BaseRelayRecipient.sol"; import { SimpleForwarder } from "../utils/SimpleForwarder.sol"; import { ERC2771Forwarder } from "../utils/ERC2771Forwarder.sol"; -import { ACL } from "../utils/ACL.sol"; +import { SimpleACL } from "../utils/SimpleACL.sol"; /** * @dev The Superfluid host implementation. @@ -61,7 +61,7 @@ contract Superfluid is ERC2771Forwarder immutable internal _ERC2771_FORWARDER; // ACL (for superapp registration) - ACL immutable internal _ACL; + SimpleACL immutable internal _SIMPLE_ACL; /** * @dev Maximum number of level of apps can be composed together @@ -112,14 +112,14 @@ contract Superfluid is uint64 callbackGasLimit, address simpleForwarderAddress, address erc2771ForwarderAddress, - address aclAddress + address simpleAclAddress ) { NON_UPGRADABLE_DEPLOYMENT = nonUpgradable; APP_WHITE_LISTING_ENABLED = appWhiteListingEnabled; CALLBACK_GAS_LIMIT = callbackGasLimit; SIMPLE_FORWARDER = SimpleForwarder(simpleForwarderAddress); _ERC2771_FORWARDER = ERC2771Forwarder(erc2771ForwarderAddress); - _ACL = ACL(aclAddress); + _SIMPLE_ACL = SimpleACL(simpleAclAddress); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -384,11 +384,11 @@ contract Superfluid is } // Checks if the deployer account has permission to register SuperApps, reverts if not. - // New method: lookup in the ACL contract. + // New method: lookup in the SimpleACL contract. // Legacy/fallback method: lookup in the governance contract. function _enforceAppRegistrationPermissioning(string memory registrationKey, address deployer) internal view { // new method: check if the deployer is granted permission in the ACL - if (_ACL.hasRole(ACL_SUPERAPP_REGISTRATION_ROLE, deployer)) { + if (_SIMPLE_ACL.hasRole(ACL_SUPERAPP_REGISTRATION_ROLE, deployer)) { return; } @@ -974,8 +974,8 @@ contract Superfluid is return address(_ERC2771_FORWARDER); } - function getACL() external view override returns(address) { - return address(_ACL); + function getSimpleACL() external view override returns(address) { + return address(_SIMPLE_ACL); } /************************************************************************** diff --git a/packages/ethereum-contracts/contracts/utils/ACL.sol b/packages/ethereum-contracts/contracts/utils/SimpleACL.sol similarity index 93% rename from packages/ethereum-contracts/contracts/utils/ACL.sol rename to packages/ethereum-contracts/contracts/utils/SimpleACL.sol index 12c4bc7306..05fb553429 100644 --- a/packages/ethereum-contracts/contracts/utils/ACL.sol +++ b/packages/ethereum-contracts/contracts/utils/SimpleACL.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.23; import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; -contract ACL is AccessControl { +contract SimpleACL is AccessControl { constructor() { _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } diff --git a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol index aad3be19ce..15cf732773 100644 --- a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol +++ b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol @@ -31,7 +31,7 @@ import { TOGA } from "./TOGA.sol"; import { IResolver } from "../interfaces/utils/IResolver.sol"; import { SimpleForwarder } from "../utils/SimpleForwarder.sol"; import { ERC2771Forwarder } from "../utils/ERC2771Forwarder.sol"; -import { ACL } from "../utils/ACL.sol"; +import { SimpleACL } from "../utils/SimpleACL.sol"; import { MacroForwarder } from "../utils/MacroForwarder.sol"; /// @title Superfluid Framework Deployment Steps @@ -144,11 +144,11 @@ contract SuperfluidFrameworkDeploymentSteps { } else if (step == 1) { // CORE CONTRACT: Superfluid (Host) SimpleForwarder simpleForwarder = new SimpleForwarder(); ERC2771Forwarder erc2771Forwarder = new ERC2771Forwarder(); - ACL acl = new ACL(); + SimpleACL simpleAcl = new SimpleACL(); // Deploy Host and initialize the test governance. // 3_000_000 is the min callback gas limit used in a prod deployment host = SuperfluidHostDeployerLibrary.deploy( - true, false, 3_000_000, address(simpleForwarder), address(erc2771Forwarder), address(acl) + true, false, 3_000_000, address(simpleForwarder), address(erc2771Forwarder), address(simpleAcl) ); simpleForwarder.transferOwnership(address(host)); erc2771Forwarder.transferOwnership(address(host)); diff --git a/packages/ethereum-contracts/ops-scripts/deploy-framework.js b/packages/ethereum-contracts/ops-scripts/deploy-framework.js index df104242a9..ac1eb10213 100644 --- a/packages/ethereum-contracts/ops-scripts/deploy-framework.js +++ b/packages/ethereum-contracts/ops-scripts/deploy-framework.js @@ -234,7 +234,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( "IAccessControlEnumerable", "SimpleForwarder", "ERC2771Forwarder", - "ACL", + "SimpleACL", ]; const mockContracts = [ "SuperfluidMock", @@ -274,7 +274,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( IAccessControlEnumerable, SimpleForwarder, ERC2771Forwarder, - ACL, + SimpleACL, } = await SuperfluidSDK.loadContracts({ ...extractWeb3Options(options), additionalContracts: contracts.concat(useMocks ? mockContracts : []), @@ -363,15 +363,15 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( console.log("ERC2771Forwarder address:", erc2771Forwarder.address); output += `ERC2771_FORWARDER=${erc2771Forwarder.address}\n`; - const acl = await web3tx(ACL.new, "ACL.new")(); - console.log("ACL address:", acl.address); - output += `ACL=${acl.address}\n`; + const simpleAcl = await web3tx(SimpleACL.new, "SimpleACL.new")(); + console.log("SimpleACL address:", simpleAcl.address); + output += `SIMPLE_ACL=${simpleAcl.address}\n`; let superfluidAddress; const superfluidLogic = await web3tx( SuperfluidLogic.new, "SuperfluidLogic.new" - )(nonUpgradable, appWhiteListing, appCallbackGasLimit, simpleForwarder.address, erc2771Forwarder.address, acl.address); + )(nonUpgradable, appWhiteListing, appCallbackGasLimit, simpleForwarder.address, erc2771Forwarder.address, simpleAcl.address); console.log( `Superfluid new code address ${superfluidLogic.address}` ); @@ -861,10 +861,10 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( "ERC2771_FORWARDER" ); - const aclAddress = await getOrDeployHelper( - ACL, - () => superfluid.getACL(), - "ACL" + const simpleAclAddress = await getOrDeployHelper( + SimpleACL, + () => superfluid.getSimpleACL(), + "SIMPLE_ACL" ); // get previous callback gas limit, make sure we don't decrease it @@ -887,14 +887,14 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( const superfluidLogic = await web3tx( SuperfluidLogic.new, "SuperfluidLogic.new" - )(nonUpgradable, appWhiteListing, appCallbackGasLimit, simpleForwarderAddress, erc2771ForwarderAddress, aclAddress); + )(nonUpgradable, appWhiteListing, appCallbackGasLimit, simpleForwarderAddress, erc2771ForwarderAddress, simpleAclAddress); output += `SUPERFLUID_HOST_LOGIC=${superfluidLogic.address}\n`; return superfluidLogic.address; }, [ ap(erc2771ForwarderAddress), ap(simpleForwarderAddress), - ap(aclAddress), + ap(simpleAclAddress), appCallbackGasLimit.toString(16).padStart(64, "0") ], ); diff --git a/packages/ethereum-contracts/tasks/bundled-abi-contracts-list.json b/packages/ethereum-contracts/tasks/bundled-abi-contracts-list.json index 40d1eae34f..2657511050 100644 --- a/packages/ethereum-contracts/tasks/bundled-abi-contracts-list.json +++ b/packages/ethereum-contracts/tasks/bundled-abi-contracts-list.json @@ -22,5 +22,5 @@ "IResolver", "Resolver", "TestResolver", "SuperfluidLoader", "IUserDefinedMacro", - "ACL" + "SimpleACL" ] diff --git a/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol index eb2e1fd7b6..41adf6aaf2 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol @@ -10,7 +10,7 @@ import { ISuperfluid, SuperAppDefinitions } from "../../../contracts/interfaces/ import { ISuperApp } from "../../../contracts/interfaces/superfluid/ISuperApp.sol"; import { AgreementMock } from "../../../contracts/mocks/AgreementMock.t.sol"; import { SuperAppMockNotSelfRegistering } from "../../../contracts/mocks/SuperAppMocks.t.sol"; -import { ACL } from "../../../contracts/utils/ACL.sol"; +import { SimpleACL } from "../../../contracts/utils/SimpleACL.sol"; contract SuperfluidIntegrationTest is FoundrySuperfluidTester { using SuperTokenV1Library for SuperToken; @@ -89,68 +89,68 @@ contract SuperfluidIntegrationTest is FoundrySuperfluidTester { vm.stopPrank(); } - function testSuperAppRegistrationViaACL() public { - ACL acl = new ACL(); - Superfluid hostWithACL = new Superfluid( - true, true, 3_000_000, address(0), address(0), address(acl) + function testSuperAppRegistrationViaSimpleACL() public { + SimpleACL simpleAcl = new SimpleACL(); + Superfluid hostWithSimpleACL = new Superfluid( + true, true, 3_000_000, address(0), address(0), address(simpleAcl) ); ISuperApp mockSuperApp1 = ISuperApp(address(new SuperAppMockNotSelfRegistering())); ISuperApp mockSuperApp2 = ISuperApp(address(new SuperAppMockNotSelfRegistering())); - hostWithACL.initialize(sf.governance); + hostWithSimpleACL.initialize(sf.governance); - bytes32 aclSuperAppRegRole = hostWithACL.ACL_SUPERAPP_REGISTRATION_ROLE(); - bytes32 aclAdminRole = acl.DEFAULT_ADMIN_ROLE(); + bytes32 aclSuperAppRegRole = hostWithSimpleACL.ACL_SUPERAPP_REGISTRATION_ROLE(); + bytes32 aclAdminRole = simpleAcl.DEFAULT_ADMIN_ROLE(); // first, give permission to alice - address aclAddress = address(hostWithACL.getACL()); + address aclAddress = address(hostWithSimpleACL.getSimpleACL()); - acl.grantRole(aclSuperAppRegRole, alice); + simpleAcl.grantRole(aclSuperAppRegRole, alice); // as bob, try to register a superapp - should revert vm.startPrank(bob); vm.expectRevert(); - hostWithACL.registerApp(mockSuperApp1, SuperAppDefinitions.APP_LEVEL_FINAL); + hostWithSimpleACL.registerApp(mockSuperApp1, SuperAppDefinitions.APP_LEVEL_FINAL); // as alice, try to register a superapp - should succeed vm.startPrank(alice); - hostWithACL.registerApp(mockSuperApp1, SuperAppDefinitions.APP_LEVEL_FINAL); + hostWithSimpleACL.registerApp(mockSuperApp1, SuperAppDefinitions.APP_LEVEL_FINAL); vm.stopPrank(); - vm.assertTrue(hostWithACL.isApp(mockSuperApp1)); + vm.assertTrue(hostWithSimpleACL.isApp(mockSuperApp1)); // revoke permission from alice - acl.revokeRole(aclSuperAppRegRole, alice); + simpleAcl.revokeRole(aclSuperAppRegRole, alice); // as alice, try to register a superapp - should revert vm.startPrank(alice); vm.expectRevert(); - hostWithACL.registerApp(mockSuperApp2, SuperAppDefinitions.APP_LEVEL_FINAL); + hostWithSimpleACL.registerApp(mockSuperApp2, SuperAppDefinitions.APP_LEVEL_FINAL); vm.stopPrank(); // nobody else can grant permission to register superapps vm.startPrank(eve); vm.expectRevert(); - acl.grantRole(aclSuperAppRegRole, bob); + simpleAcl.grantRole(aclSuperAppRegRole, bob); vm.stopPrank(); // nobody can define admin roles ... bytes32 dedicatedAdminRole = keccak256("SUPER_APP_REGISTRATION_ADMIN"); vm.startPrank(eve); vm.expectRevert(); - acl.setRoleAdmin(aclSuperAppRegRole, dedicatedAdminRole); + simpleAcl.setRoleAdmin(aclSuperAppRegRole, dedicatedAdminRole); vm.stopPrank(); // ... except the default admin // (This is to be done in a possible future where we start using this ACL for other purposes too // and want to have a more sophisticated permissioning scheme.) - acl.setRoleAdmin(aclSuperAppRegRole, dedicatedAdminRole); + simpleAcl.setRoleAdmin(aclSuperAppRegRole, dedicatedAdminRole); // grant heidi the new admin role - acl.grantRole(dedicatedAdminRole, heidi); + simpleAcl.grantRole(dedicatedAdminRole, heidi); // now heidi can manage permissions for superapp deployers vm.startPrank(heidi); - acl.grantRole(aclSuperAppRegRole, bob); + simpleAcl.grantRole(aclSuperAppRegRole, bob); vm.stopPrank(); } } From 09c1de0375a1f279f61ade2028a1935403fc0fd8 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 1 Jul 2025 18:15:31 +0200 Subject: [PATCH 21/25] more adjustments --- .../contracts/mocks/SuperfluidMock.t.sol | 4 ++-- .../utils/SuperfluidFrameworkDeploymentSteps.t.sol | 4 ++-- .../test/contracts/superfluid/Superfluid.test.ts | 9 ++++++--- .../test/foundry/superfluid/Superfluid.t.sol | 3 --- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/ethereum-contracts/contracts/mocks/SuperfluidMock.t.sol b/packages/ethereum-contracts/contracts/mocks/SuperfluidMock.t.sol index 606d2d5f89..9ed7ac4183 100644 --- a/packages/ethereum-contracts/contracts/mocks/SuperfluidMock.t.sol +++ b/packages/ethereum-contracts/contracts/mocks/SuperfluidMock.t.sol @@ -136,11 +136,11 @@ contract SuperfluidMock is Superfluid { uint64 callbackGasLimit, address simpleForwarderAddress, address erc2771ForwarderAddress, - address allowListAddress + address simpleAclAddress ) Superfluid( nonUpgradable, appWhiteListingEnabled, callbackGasLimit, simpleForwarderAddress, erc2771ForwarderAddress, - allowListAddress + simpleAclAddress ) // solhint-disable-next-line no-empty-blocks { diff --git a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol index 15cf732773..0276d399f0 100644 --- a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol +++ b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol @@ -322,13 +322,13 @@ library SuperfluidHostDeployerLibrary { uint64 callbackGasLimit, address simpleForwarderAddress, address erc2771ForwarderAddress, - address allowListAddress + address simpleAclAddress ) external returns (Superfluid) { return new Superfluid( _nonUpgradable, _appWhiteListingEnabled, callbackGasLimit, simpleForwarderAddress, erc2771ForwarderAddress, - allowListAddress + simpleAclAddress ); } } diff --git a/packages/ethereum-contracts/test/contracts/superfluid/Superfluid.test.ts b/packages/ethereum-contracts/test/contracts/superfluid/Superfluid.test.ts index 6a453c81dc..16cac2cdd0 100644 --- a/packages/ethereum-contracts/test/contracts/superfluid/Superfluid.test.ts +++ b/packages/ethereum-contracts/test/contracts/superfluid/Superfluid.test.ts @@ -115,14 +115,16 @@ describe("Superfluid Host Contract", function () { false /* appWhiteListingEnabled */, 3000000 /* callbackGasLimit */, ZERO_ADDRESS /* simpleForwarder */, - ZERO_ADDRESS /* erc2771Forwarder */ + ZERO_ADDRESS /* erc2771Forwarder */, + ZERO_ADDRESS /* simpleAcl */ ); const mock2 = await sfMockFactory.deploy( true /* nonUpgradable */, false /* appWhiteListingEnabled */, 3000000 /* callbackGasLimit */, ZERO_ADDRESS /* simpleForwarder */, - ZERO_ADDRESS /* erc2771Forwarder */ + ZERO_ADDRESS /* erc2771Forwarder */, + ZERO_ADDRESS /* simpleAcl */ ); await governance.updateContracts( superfluid.address, @@ -2696,7 +2698,8 @@ describe("Superfluid Host Contract", function () { false /* appWhiteListingEnabled */, 3000000 /* callbackGasLimit */, ZERO_ADDRESS /* simpleForwarder */, - ZERO_ADDRESS /* erc2771Forwarder */ + ZERO_ADDRESS /* erc2771Forwarder */, + ZERO_ADDRESS /* simpleAcl */ ); await expectCustomError( governance.updateContracts( diff --git a/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol index 41adf6aaf2..9721897ca6 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.t.sol @@ -100,11 +100,8 @@ contract SuperfluidIntegrationTest is FoundrySuperfluidTester { hostWithSimpleACL.initialize(sf.governance); bytes32 aclSuperAppRegRole = hostWithSimpleACL.ACL_SUPERAPP_REGISTRATION_ROLE(); - bytes32 aclAdminRole = simpleAcl.DEFAULT_ADMIN_ROLE(); // first, give permission to alice - address aclAddress = address(hostWithSimpleACL.getSimpleACL()); - simpleAcl.grantRole(aclSuperAppRegRole, alice); // as bob, try to register a superapp - should revert From 39fb8a9394530aecf04ebe1180e196e37d7cd4d2 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 1 Jul 2025 18:38:38 +0200 Subject: [PATCH 22/25] fix test --- .../ethereum-contracts/contracts/mocks/SuperAppMocks.t.sol | 7 +++---- .../ethereum-contracts/ops-scripts/deploy-framework.js | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ethereum-contracts/contracts/mocks/SuperAppMocks.t.sol b/packages/ethereum-contracts/contracts/mocks/SuperAppMocks.t.sol index 5cbf762bbe..17791b9ca2 100644 --- a/packages/ethereum-contracts/contracts/mocks/SuperAppMocks.t.sol +++ b/packages/ethereum-contracts/contracts/mocks/SuperAppMocks.t.sol @@ -60,7 +60,6 @@ contract SuperAppMock is ISuperApp { } function tryRegisterApp(uint256 configWord) external { - // @note this is deprecated keeping this here for testing/coverage _host.registerApp(configWord); } @@ -615,10 +614,9 @@ contract SuperAppMockWithRegistrationKey { } } -// An Super App that uses registerAppWithKey +// An Super App that self-registers contract SuperAppMockUsingRegisterApp { constructor(ISuperfluid host, uint256 configWord) { - // @note this is deprecated keeping this here for testing/coverage host.registerApp(configWord); } } @@ -630,6 +628,7 @@ contract SuperAppMockNotSelfRegistering { } // Factory which allows anybody to deploy arbitrary contracts as app (do NOT allow this in a real factory!) contract SuperAppFactoryMock { function registerAppWithHost(ISuperfluid host, ISuperApp app, uint256 configWord) external { - host.registerApp(app, configWord); + // @note this way of registering is DEPREACTED! + host.registerAppByFactory(app, configWord); } } diff --git a/packages/ethereum-contracts/ops-scripts/deploy-framework.js b/packages/ethereum-contracts/ops-scripts/deploy-framework.js index ac1eb10213..850c977500 100644 --- a/packages/ethereum-contracts/ops-scripts/deploy-framework.js +++ b/packages/ethereum-contracts/ops-scripts/deploy-framework.js @@ -866,6 +866,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function ( () => superfluid.getSimpleACL(), "SIMPLE_ACL" ); + console.log("SimpleACL address", simpleAclAddress); // get previous callback gas limit, make sure we don't decrease it const prevCallbackGasLimit = await superfluid.CALLBACK_GAS_LIMIT(); From 64a187943a2056bbdcc7742a6d1029dffaed516e Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 1 Jul 2025 18:51:17 +0200 Subject: [PATCH 23/25] text fix --- .../ethereum-contracts/contracts/apps/SuperTokenV1Library.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol b/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol index 4c51580392..529ec77d57 100644 --- a/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol +++ b/packages/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol @@ -47,7 +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: - * - avoid higher-level library methods which can themselves trigger reverts in tests where this is is an issue + * - 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 * Also be aware of other limitations, see From ede6643862f6211014304156645932c4aaa89f72 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 1 Jul 2025 19:17:44 +0200 Subject: [PATCH 24/25] one more fix --- .../ethereum-contracts/test/ops-scripts/deployment.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ethereum-contracts/test/ops-scripts/deployment.test.js b/packages/ethereum-contracts/test/ops-scripts/deployment.test.js index 5a0e7da488..d026d6f47d 100644 --- a/packages/ethereum-contracts/test/ops-scripts/deployment.test.js +++ b/packages/ethereum-contracts/test/ops-scripts/deployment.test.js @@ -128,7 +128,8 @@ contract("Embedded deployment scripts", (accounts) => { false, // appWhiteListingEnabled callbackGasLimit, // callbackGasLimit ZERO_ADDRESS, // simpleForwarder - ZERO_ADDRESS // erc2771Forwarder + ZERO_ADDRESS, // erc2771Forwarder + ZERO_ADDRESS // simpleAcl ); assert.isFalse( await codeChanged(web3, Superfluid, a1.address, [ From e14c5628ff04a0f72312aea698db60cc22e04641 Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 2 Jul 2025 16:55:20 +0200 Subject: [PATCH 25/25] change return type of getSimpleACL --- .../interfaces/superfluid/ISuperfluid.sol | 14 +++++++------- .../contracts/superfluid/Superfluid.sol | 7 ++++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol index 7a43da35c2..d04c83ed95 100644 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol +++ b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol @@ -18,6 +18,7 @@ import { /// Note: CustomSuperTokenBase is not included for people building CustomSuperToken. import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { IERC777 } from "@openzeppelin/contracts/token/ERC777/IERC777.sol"; +import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; import { ISuperfluidToken } from "./ISuperfluidToken.sol"; import { ISuperToken } from "./ISuperToken.sol"; import { ISuperTokenFactory } from "./ISuperTokenFactory.sol"; @@ -29,10 +30,10 @@ import { IPoolMemberNFT } from "../agreements/gdav1/IPoolMemberNFT.sol"; import { ISuperAgreement } from "./ISuperAgreement.sol"; import { IConstantFlowAgreementV1 } from "../agreements/IConstantFlowAgreementV1.sol"; import { IInstantDistributionAgreementV1 } from "../agreements/IInstantDistributionAgreementV1.sol"; -import { - IGeneralDistributionAgreementV1, - PoolConfig, - PoolERC20Metadata +import { + IGeneralDistributionAgreementV1, + PoolConfig, + PoolERC20Metadata } from "../agreements/gdav1/IGeneralDistributionAgreementV1.sol"; import { ISuperfluidPool } from "../agreements/gdav1/ISuperfluidPool.sol"; /// Superfluid App interfaces: @@ -660,7 +661,7 @@ interface ISuperfluid { // solhint-disable max-line-length /** - * @dev returns the address of the SimpleACL contract (currently used for SuperApp registration permissioning). + * @dev returns the SimpleACL contract (currently used for SuperApp registration permissioning). * That contract implements the interface [IAccessControl](https://docs.openzeppelin.com/contracts/4.x/api/access#IAccessControl), * which provides the following functions: * - [hasRole(role, account)](https://docs.openzeppelin.com/contracts/4.x/api/access#IAccessControl-hasRole-bytes32-address-) @@ -668,9 +669,8 @@ interface ISuperfluid { * - [grantRole(role, account)](https://docs.openzeppelin.com/contracts/4.x/api/access#IAccessControl-grantRole-bytes32-address-) * - [revokeRole(role, account)](https://docs.openzeppelin.com/contracts/4.x/api/access#IAccessControl-revokeRole-bytes32-address-) * - [renounceRole(role, account)](https://docs.openzeppelin.com/contracts/4.x/api/access#IAccessControl-renounceRole-bytes32-address-) - * @return address of the SimpleACL contract */ - function getSimpleACL() external view returns(address); + function getSimpleACL() external view returns(IAccessControl); /************************************************************************** * Function modifiers for access control and parameter validations diff --git a/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol b/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol index 53c995c21e..7df746f332 100644 --- a/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol +++ b/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol @@ -16,7 +16,8 @@ import { SuperfluidGovernanceConfigs, ISuperfluidToken, ISuperToken, - ISuperTokenFactory + ISuperTokenFactory, + IAccessControl } from "../interfaces/superfluid/ISuperfluid.sol"; import { GeneralDistributionAgreementV1 } from "../agreements/gdav1/GeneralDistributionAgreementV1.sol"; import { SuperfluidUpgradeableBeacon } from "../upgradability/SuperfluidUpgradeableBeacon.sol"; @@ -974,8 +975,8 @@ contract Superfluid is return address(_ERC2771_FORWARDER); } - function getSimpleACL() external view override returns(address) { - return address(_SIMPLE_ACL); + function getSimpleACL() external view override returns(IAccessControl) { + return _SIMPLE_ACL; } /**************************************************************************