Skip to content

Commit 8ac8ca4

Browse files
authored
[ETHEREUM-CONTRACTS] v1.13.0: SuperfluidPool: add [increase|decrease]MemberUnits and deprecate PoolMemberNFT (#2077)
* added increaseMemberUnits and decreaseMemberUnits to ISuperfluidPool * added burn method to PoolMemberNFT * added burn method to PoolMemberNFT
1 parent 839fc6d commit 8ac8ca4

File tree

18 files changed

+146
-214
lines changed

18 files changed

+146
-214
lines changed

packages/automation-contracts/autowrap/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"version": "0.3.0",
55
"devDependencies": {
66
"@openzeppelin/contracts": "^4.9.6",
7-
"@superfluid-finance/ethereum-contracts": "^1.12.1",
7+
"@superfluid-finance/ethereum-contracts": "^1.13.0",
88
"@superfluid-finance/metadata": "^1.6.0"
99
},
1010
"license": "MIT",

packages/automation-contracts/scheduler/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"version": "1.3.0",
55
"devDependencies": {
66
"@openzeppelin/contracts": "^4.9.6",
7-
"@superfluid-finance/ethereum-contracts": "^1.12.1",
7+
"@superfluid-finance/ethereum-contracts": "^1.13.0",
88
"@superfluid-finance/metadata": "^1.6.0"
99
},
1010
"license": "MIT",

packages/ethereum-contracts/CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ All notable changes to the ethereum-contracts will be documented in this file.
33

44
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
55

6-
## [unreleased]
6+
## [v1.13.0]
77

88
### Added
9-
- `SuperToken` now implements [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) (permit extension for EIP-20 signed approvals)
9+
- `SuperToken` now implements [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) (permit extension for EIP-20 signed approvals).
10+
- `SuperfluidPool` now has additional methods `increaseMemberUnits` and `decreaseMemberUnits` which allow the pool admin to change member units parameterized with delta amounts.
11+
12+
### Breaking
13+
- `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).
1014

1115
## [v1.12.1]
1216

packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,27 @@ import { AgreementBase } from "../AgreementBase.sol";
3232
import { AgreementLibrary } from "../AgreementLibrary.sol";
3333

3434

35+
/// @dev Universal Index state slot id for storing universal index data
36+
function _universalIndexStateSlotId() pure returns (uint256) {
37+
return 0;
38+
}
39+
40+
/// @dev returns true if the account is a pool
41+
function _isPool(
42+
IGeneralDistributionAgreementV1 gda,
43+
ISuperfluidToken token,
44+
address account
45+
) view returns (bool exists) {
46+
// solhint-disable var-name-mixedcase
47+
// @note see createPool, we retrieve the isPool bit from
48+
// UniversalIndex for this pool to determine whether the account
49+
// is a pool
50+
exists = (
51+
(uint256(token.getAgreementStateSlot(address(gda), account, _universalIndexStateSlotId(), 1)[0]) << 224)
52+
>> 224
53+
) & 1 == 1;
54+
}
55+
3556
/**
3657
* @title General Distribution Agreement
3758
* @author Superfluid
@@ -41,7 +62,7 @@ import { AgreementLibrary } from "../AgreementLibrary.sol";
4162
* Agreement State
4263
*
4364
* Universal Index Data
44-
* slotId = _UNIVERSAL_INDEX_STATE_SLOT_ID or 0
65+
* slotId = _universalIndexStateSlotId() or 0
4566
* msg.sender = address of GDAv1
4667
* account = context.msgSender
4768
* 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
103124

104125
address public constant SUPERFLUID_POOL_DEPLOYER_ADDRESS = address(SuperfluidPoolDeployerLibrary);
105126

106-
/// @dev Universal Index state slot id for storing universal index data
107-
uint256 private constant _UNIVERSAL_INDEX_STATE_SLOT_ID = 0;
108127
/// @dev Pool member state slot id for storing subs bitmap
109128
uint256 private constant _POOL_SUBS_BITMAP_STATE_SLOT_ID = 1;
110129
/// @dev Pool member state slot id starting point for pool connections
@@ -127,7 +146,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
127146
{
128147
UniversalIndexData memory universalIndexData = _getUIndexData(abi.encode(token), account);
129148

130-
if (_isPool(token, account)) {
149+
if (_isPool(this, token, account)) {
131150
rtb = ISuperfluidPool(account).getDisconnectedBalance(uint32(time));
132151
} else {
133152
rtb = Value.unwrap(_getBasicParticleFromUIndex(universalIndexData).rtb(Time.wrap(uint32(time))));
@@ -165,7 +184,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
165184
function getNetFlow(ISuperfluidToken token, address account) external view override returns (int96 netFlowRate) {
166185
netFlowRate = int256(FlowRate.unwrap(_getUIndex(abi.encode(token), account).flow_rate())).toInt96();
167186

168-
if (_isPool(token, account)) {
187+
if (_isPool(this, token, account)) {
169188
netFlowRate += ISuperfluidPool(account).getTotalDisconnectedFlowRate();
170189
}
171190

@@ -274,7 +293,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
274293
) internal returns (ISuperfluidPool pool) {
275294
// @note ensure if token and admin are the same that nothing funky happens with echidna
276295
if (admin == address(0)) revert GDA_NO_ZERO_ADDRESS_ADMIN();
277-
if (_isPool(token, admin)) revert GDA_ADMIN_CANNOT_BE_POOL();
296+
if (_isPool(this, token, admin)) revert GDA_ADMIN_CANNOT_BE_POOL();
278297

279298
pool = ISuperfluidPool(
280299
address(
@@ -288,7 +307,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
288307
// to store whether an account is a pool or not
289308
bytes32[] memory data = new bytes32[](1);
290309
data[0] = bytes32(uint256(1));
291-
token.updateAgreementStateSlot(address(pool), _UNIVERSAL_INDEX_STATE_SLOT_ID, data);
310+
token.updateAgreementStateSlot(address(pool), _universalIndexStateSlotId(), data);
292311

293312
IPoolAdminNFT poolAdminNFT = IPoolAdminNFT(_getPoolAdminNFTAddress(token));
294313

@@ -409,7 +428,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
409428
}
410429

411430
function appendIndexUpdateByPool(ISuperfluidToken token, BasicParticle memory p, Time t) external returns (bool) {
412-
if (_isPool(token, msg.sender) == false) {
431+
if (_isPool(this, token, msg.sender) == false) {
413432
revert GDA_ONLY_SUPER_TOKEN_POOL();
414433
}
415434
bytes memory eff = abi.encode(token);
@@ -422,7 +441,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
422441
external
423442
returns (bool)
424443
{
425-
if (_isPool(superToken, msg.sender) == false) {
444+
if (_isPool(this, superToken, msg.sender) == false) {
426445
revert GDA_ONLY_SUPER_TOKEN_POOL();
427446
}
428447

@@ -443,7 +462,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
443462

444463
newCtx = ctx;
445464

446-
if (_isPool(token, address(pool)) == false ||
465+
if (_isPool(this, token, address(pool)) == false ||
447466
// Note: we do not support multi-tokens pools
448467
pool.superToken() != token) {
449468
revert GDA_ONLY_SUPER_TOKEN_POOL();
@@ -509,7 +528,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
509528
int96 requestedFlowRate,
510529
bytes calldata ctx
511530
) external override returns (bytes memory newCtx) {
512-
if (_isPool(token, address(pool)) == false ||
531+
if (_isPool(this, token, address(pool)) == false ||
513532
// Note: we do not support multi-tokens pools
514533
pool.superToken() != token) {
515534
revert GDA_ONLY_SUPER_TOKEN_POOL();
@@ -708,7 +727,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
708727
// new buffer
709728
(universalIndexData.totalBuffer.toInt256() + Value.unwrap(bufferDelta)).toUint256();
710729
ISuperfluidToken(token).updateAgreementStateSlot(
711-
from, _UNIVERSAL_INDEX_STATE_SLOT_ID, _encodeUniversalIndexData(universalIndexData)
730+
from, _universalIndexStateSlotId(), _encodeUniversalIndexData(universalIndexData)
712731
);
713732

714733
{
@@ -837,7 +856,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
837856
{
838857
(, universalIndexData) = _decodeUniversalIndexData(
839858
ISuperfluidToken(abi.decode(eff, (address))).getAgreementStateSlot(
840-
address(this), owner, _UNIVERSAL_INDEX_STATE_SLOT_ID, 2
859+
address(this), owner, _universalIndexStateSlotId(), 2
841860
)
842861
);
843862
}
@@ -856,7 +875,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
856875
function _getUIndex(bytes memory eff, address owner) internal view override returns (BasicParticle memory uIndex) {
857876
(, UniversalIndexData memory universalIndexData) = _decodeUniversalIndexData(
858877
ISuperfluidToken(abi.decode(eff, (address))).getAgreementStateSlot(
859-
address(this), owner, _UNIVERSAL_INDEX_STATE_SLOT_ID, 2
878+
address(this), owner, _universalIndexStateSlotId(), 2
860879
)
861880
);
862881
uIndex = _getBasicParticleFromUIndex(universalIndexData);
@@ -871,7 +890,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
871890

872891
ISuperfluidToken(abi.decode(eff, (address))).updateAgreementStateSlot(
873892
owner,
874-
_UNIVERSAL_INDEX_STATE_SLOT_ID,
893+
_universalIndexStateSlotId(),
875894
_encodeUniversalIndexData(p, universalIndexData.totalBuffer, universalIndexData.isPool)
876895
);
877896

@@ -989,17 +1008,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
9891008

9901009
/// @inheritdoc IGeneralDistributionAgreementV1
9911010
function isPool(ISuperfluidToken token, address account) external view override returns (bool) {
992-
return _isPool(token, account);
993-
}
994-
995-
function _isPool(ISuperfluidToken token, address account) internal view returns (bool exists) {
996-
// @note see createPool, we retrieve the isPool bit from
997-
// UniversalIndex for this pool to determine whether the account
998-
// is a pool
999-
exists = (
1000-
(uint256(token.getAgreementStateSlot(address(this), account, _UNIVERSAL_INDEX_STATE_SLOT_ID, 1)[0]) << 224)
1001-
>> 224
1002-
) & 1 == 1;
1011+
return _isPool(this, token, account);
10031012
}
10041013

10051014
// FlowDistributionData data packing:

packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { IGeneralDistributionAgreementV1, ISuperfluid } from "../../interfaces/s
88
import { ISuperfluidPool } from "../../interfaces/agreements/gdav1/ISuperfluidPool.sol";
99
import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol";
1010

11+
/// DEPRECATED - the update hooks are no longer invoked.
1112
contract PoolMemberNFT is PoolNFTBase, IPoolMemberNFT {
1213
//// Storage Variables ////
1314

@@ -136,4 +137,22 @@ contract PoolMemberNFT is PoolNFTBase, IPoolMemberNFT {
136137
// emit burn of pool member token with tokenId
137138
emit Transfer(owner, address(0), tokenId);
138139
}
140+
141+
/// This was added after deprecating the PoolMemberNFT.
142+
/// It allows owners of such tokens to get rid of them
143+
/// in case it bothers them (e.g. cluttering the wallet).
144+
function burn(uint256 tokenId) external {
145+
address owner = _ownerOf(tokenId);
146+
if (msg.sender != owner) {
147+
revert POOL_MEMBER_NFT_ONLY_OWNER();
148+
}
149+
150+
super._burn(tokenId);
151+
152+
// remove previous tokenId flow data mapping
153+
delete _poolMemberDataByTokenId[tokenId];
154+
155+
// emit burn of pool member token with tokenId
156+
emit Transfer(owner, address(0), tokenId);
157+
}
139158
}

packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol

Lines changed: 24 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ import {
1919
} from "@superfluid-finance/solidity-semantic-money/src/SemanticMoney.sol";
2020
import { ISuperfluid } from "../../interfaces/superfluid/ISuperfluid.sol";
2121
import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol";
22-
import { ISuperToken } from "../../interfaces/superfluid/ISuperToken.sol";
2322
import { ISuperfluidPool } from "../../interfaces/agreements/gdav1/ISuperfluidPool.sol";
24-
import { GeneralDistributionAgreementV1 } from "../../agreements/gdav1/GeneralDistributionAgreementV1.sol";
23+
import {
24+
GeneralDistributionAgreementV1, _isPool } from "../../agreements/gdav1/GeneralDistributionAgreementV1.sol";
2525
import { BeaconProxiable } from "../../upgradability/BeaconProxiable.sol";
26-
import { IPoolMemberNFT } from "../../interfaces/agreements/gdav1/IPoolMemberNFT.sol";
2726

2827
using SafeCast for uint256;
2928
using SafeCast for int256;
@@ -384,7 +383,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable {
384383

385384
/// @inheritdoc ISuperfluidPool
386385
function updateMemberUnits(address memberAddr, uint128 newUnits) external returns (bool) {
387-
if (msg.sender != admin && msg.sender != address(GDA)) revert SUPERFLUID_POOL_NOT_POOL_ADMIN_OR_GDA();
386+
_enforceChangeMemberUnitsPreconditions();
388387

389388
uint128 oldUnits = _updateMemberUnits(memberAddr, newUnits);
390389

@@ -399,52 +398,34 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable {
399398
return true;
400399
}
401400

402-
/**
403-
* @notice Checks whether or not the NFT hook can be called.
404-
* @dev A staticcall, so `POOL_MEMBER_NFT` must be a view otherwise the assumption is that it reverts
405-
* @param token the super token that is being streamed
406-
* @return poolMemberNFT the address returned by low level call
407-
*/
408-
function _canCallNFTHook(ISuperfluidToken token) internal view returns (address poolMemberNFT) {
409-
// solhint-disable-next-line avoid-low-level-calls
410-
(bool success, bytes memory data) =
411-
address(token).staticcall(abi.encodeWithSelector(ISuperToken.POOL_MEMBER_NFT.selector));
412-
413-
if (success) {
414-
// @note We are aware this may revert if a Custom SuperToken's
415-
// POOL_MEMBER_NFT does not return data that can be
416-
// decoded to an address. This would mean it was intentionally
417-
// done by the creator of the Custom SuperToken logic and is
418-
// fully expected to revert in that case as the author desired.
419-
poolMemberNFT = abi.decode(data, (address));
420-
}
401+
/// @inheritdoc ISuperfluidPool
402+
function increaseMemberUnits(address memberAddr, uint128 addedUnits) external override returns (bool) {
403+
_enforceChangeMemberUnitsPreconditions();
404+
405+
_updateMemberUnits(memberAddr, _getUnits(memberAddr) + addedUnits);
406+
emit Transfer(address(0), memberAddr, addedUnits);
407+
408+
return true;
421409
}
422410

423-
function _handlePoolMemberNFT(address memberAddr, uint128 newUnits) internal {
424-
// Pool Member NFT Logic
425-
IPoolMemberNFT poolMemberNFT = IPoolMemberNFT(_canCallNFTHook(superToken));
426-
if (address(poolMemberNFT) != address(0)) {
427-
uint256 tokenId = poolMemberNFT.getTokenId(address(this), memberAddr);
428-
if (newUnits == 0) {
429-
if (poolMemberNFT.poolMemberDataByTokenId(tokenId).member != address(0)) {
430-
poolMemberNFT.onDelete(address(this), memberAddr);
431-
}
432-
} else {
433-
// if not minted, we mint a new pool member nft
434-
if (poolMemberNFT.poolMemberDataByTokenId(tokenId).member == address(0)) {
435-
poolMemberNFT.onCreate(address(this), memberAddr);
436-
} else {
437-
// if minted, we update the pool member nft
438-
poolMemberNFT.onUpdate(address(this), memberAddr);
439-
}
440-
}
441-
}
411+
/// @inheritdoc ISuperfluidPool
412+
function decreaseMemberUnits(address memberAddr, uint128 subtractedUnits) external override returns (bool) {
413+
_enforceChangeMemberUnitsPreconditions();
414+
415+
_updateMemberUnits(memberAddr, _getUnits(memberAddr) - subtractedUnits);
416+
emit Transfer(memberAddr, address(0), subtractedUnits);
417+
418+
return true;
419+
}
420+
421+
function _enforceChangeMemberUnitsPreconditions() internal view {
422+
if (msg.sender != admin && msg.sender != address(GDA)) revert SUPERFLUID_POOL_NOT_POOL_ADMIN_OR_GDA();
442423
}
443424

444425
function _updateMemberUnits(address memberAddr, uint128 newUnits) internal returns (uint128 oldUnits) {
445426
// @note normally we keep the sanitization in the external functions, but here
446427
// this is used in both updateMemberUnits and transfer
447-
if (GDA.isPool(superToken, memberAddr)) revert SUPERFLUID_POOL_NO_POOL_MEMBERS();
428+
if (_isPool(GDA, superToken, memberAddr)) revert SUPERFLUID_POOL_NO_POOL_MEMBERS();
448429
if (memberAddr == address(0)) revert SUPERFLUID_POOL_NO_ZERO_ADDRESS();
449430

450431
uint32 time = uint32(ISuperfluid(superToken.getHost()).getNow());
@@ -474,8 +455,6 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable {
474455
assert(GDA.appendIndexUpdateByPool(superToken, p, t));
475456
}
476457
emit MemberUnitsUpdated(superToken, memberAddr, oldUnits, newUnits);
477-
478-
_handlePoolMemberNFT(memberAddr, newUnits);
479458
}
480459

481460
function _claimAll(address memberAddr, uint32 time) internal returns (int256 amount) {

packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IPoolMemberNFT.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,17 @@ interface IPoolMemberNFT is IPoolNFTBase {
2424
error POOL_MEMBER_NFT_NO_ZERO_MEMBER();
2525
error POOL_MEMBER_NFT_NO_UNITS();
2626
error POOL_MEMBER_NFT_HAS_UNITS();
27+
error POOL_MEMBER_NFT_ONLY_OWNER();
2728

2829
function onCreate(address pool, address member) external;
2930

3031
function onUpdate(address pool, address member) external;
3132

3233
function onDelete(address pool, address member) external;
3334

35+
/// Allows the owner to burn their token
36+
function burn(uint256 tokenId) external;
37+
3438
/// View Functions ///
3539

3640
function poolMemberDataByTokenId(uint256 tokenId) external view returns (PoolMemberNFTData memory data);

packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ interface ISuperfluidPool is IERC20, IERC20Metadata {
8989
/// @param newUnits The new units for the member
9090
function updateMemberUnits(address memberAddr, uint128 newUnits) external returns (bool);
9191

92+
/// @notice Increases `memberAddr` ownedUnits by `addedUnits`
93+
/// @param memberAddr The address of the member
94+
/// @param addedUnits The additional units for the member
95+
function increaseMemberUnits(address memberAddr, uint128 addedUnits) external returns (bool);
96+
97+
/// @notice Decreases `memberAddr` ownedUnits by `subtractedUnits`
98+
/// @param memberAddr The address of the member
99+
/// @param subtractedUnits The units subtracted for the member
100+
function decreaseMemberUnits(address memberAddr, uint128 subtractedUnits) external returns (bool);
101+
92102
/// @notice Claims the claimable balance for `memberAddr` at `block.timestamp`
93103
/// @param memberAddr The address of the member
94104
function claimAll(address memberAddr) external returns (bool);

packages/ethereum-contracts/ops-scripts/deploy-framework.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1198,7 +1198,7 @@ module.exports = eval(`(${S.toString()})({skipArgv: true})`)(async function (
11981198

11991199
// initialize the proxy contracts with the nft names
12001200
await poolAdminNFT.initialize("Pool Admin NFT", "PA");
1201-
await poolMemberNFT.initialize("Pool Member NFT", "PM");
1201+
await poolMemberNFT.initialize("Pool Member NFT (deprecated)", "PM");
12021202

12031203
// set the nft proxy addresses (to be consumed by the super token logic constructor)
12041204
poolAdminNFTProxyAddress = poolAdminNFTProxy.address;

packages/ethereum-contracts/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@superfluid-finance/ethereum-contracts",
33
"description": " Ethereum contracts implementation for the Superfluid Protocol",
4-
"version": "1.12.1",
4+
"version": "1.13.0",
55
"dependencies": {
66
"@decentral.ee/web3-helpers": "0.5.3",
77
"@nomiclabs/hardhat-ethers": "2.2.3",

0 commit comments

Comments
 (0)