Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
697b97d
added increaseMemberUnits and decreaseMemberUnits to ISuperfluidPool
d10r Jun 13, 2025
912f970
added burn method to PoolMemberNFT
d10r Jun 16, 2025
85a30d4
added burn method to PoolMemberNFT
d10r Jun 16, 2025
eb1b6bd
duplicate _isPool
d10r Jun 16, 2025
2722583
deprecate PoolMemberNFT
d10r Jun 16, 2025
9820c92
bump version to 1.13.0
d10r Jun 16, 2025
a0aed70
change PoolMemberNFT token name
d10r Jun 16, 2025
2bf628a
adjust tests
d10r Jun 16, 2025
45fbbd8
updated changelog
d10r Jun 16, 2025
5634adc
added test
d10r Jun 16, 2025
9c6cc97
fix flaky test
d10r Jun 17, 2025
1e56be3
Merge branch 'dev' into 2025-06-incdec_poolunits
d10r Jun 17, 2025
a4cee27
more sensible gas price defaults for testnets (too)
d10r Jun 17, 2025
35e54ee
convert _isPool to free function so it can be shared
d10r Jun 18, 2025
b3dcea2
removed duplicate line
d10r Jun 24, 2025
7a23d38
mocks shall not use the deprecated method anymore
d10r Jun 24, 2025
9d39c49
add dedicated allowlist for superapp registration
d10r Jun 24, 2025
959e9d3
test revoke too
d10r Jun 24, 2025
eac3fe0
Merge branch 'dev' into superapp-allowlist
d10r Jun 30, 2025
131d4a3
Merge branch 'dev' into superapp-allowlist
d10r Jun 30, 2025
a2f295c
use OZ AccessControl instead of a custom implementation
d10r Jul 1, 2025
ce53cea
updated CHANGELOG
d10r Jul 1, 2025
fbf3ad4
rename to SimpleACL
d10r Jul 1, 2025
09c1de0
more adjustments
d10r Jul 1, 2025
39fb8a9
fix test
d10r Jul 1, 2025
64a1879
text fix
d10r Jul 1, 2025
ede6643
one more fix
d10r Jul 1, 2025
e14c562
change return type of getSimpleACL
d10r Jul 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/automation-contracts/autowrap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/automation-contracts/scheduler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 6 additions & 2 deletions packages/ethereum-contracts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ 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]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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))));
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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(
Expand All @@ -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));

Expand Down Expand Up @@ -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);
Expand All @@ -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();
}

Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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)
);

{
Expand Down Expand Up @@ -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
)
);
}
Expand All @@ -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);
Expand All @@ -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)
);

Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ////

Expand Down Expand Up @@ -136,4 +137,22 @@ 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();
}

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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ 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 {
GeneralDistributionAgreementV1, _isPool } 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;
Expand Down Expand Up @@ -384,7 +383,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);

Expand All @@ -399,52 +398,34 @@ 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));
}
/// @inheritdoc ISuperfluidPool
function increaseMemberUnits(address memberAddr, uint128 addedUnits) external override returns (bool) {
_enforceChangeMemberUnitsPreconditions();

_updateMemberUnits(memberAddr, _getUnits(memberAddr) + addedUnits);
emit Transfer(address(0), memberAddr, addedUnits);

return true;
}

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);
}
}
}
/// @inheritdoc ISuperfluidPool
function decreaseMemberUnits(address memberAddr, uint128 subtractedUnits) external override returns (bool) {
_enforceChangeMemberUnitsPreconditions();

_updateMemberUnits(memberAddr, _getUnits(memberAddr) - subtractedUnits);
emit Transfer(memberAddr, address(0), subtractedUnits);

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
if (GDA.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());
Expand Down Expand Up @@ -474,8 +455,6 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable {
assert(GDA.appendIndexUpdateByPool(superToken, p, t));
}
emit MemberUnitsUpdated(superToken, memberAddr, oldUnits, newUnits);

_handlePoolMemberNFT(memberAddr, newUnits);
}

function _claimAll(address memberAddr, uint32 time) internal returns (int256 amount) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ 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;

function onUpdate(address pool, address member) external;

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
Loading
Loading