Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
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: 7 additions & 1 deletion packages/ethereum-contracts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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 @@
// 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();

Check warning on line 147 in packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol

View check run for this annotation

Codecov / codecov/patch

packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol#L144-L147

Added lines #L144 - L147 were not covered by tests
}

super._burn(tokenId);

Check warning on line 150 in packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol

View check run for this annotation

Codecov / codecov/patch

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

Added line #L150 was not covered by tests

// remove previous tokenId flow data mapping
delete _poolMemberDataByTokenId[tokenId];

Check warning on line 153 in packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol

View check run for this annotation

Codecov / codecov/patch

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

Added line #L153 was not covered by tests

// emit burn of pool member token with tokenId
emit Transfer(owner, address(0), tokenId);

Check warning on line 156 in packages/ethereum-contracts/contracts/agreements/gdav1/PoolMemberNFT.sol

View check run for this annotation

Codecov / codecov/patch

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

Added line #L156 was not covered by tests
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -384,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);

Expand All @@ -399,52 +397,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(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 +454,19 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable {
assert(GDA.appendIndexUpdateByPool(superToken, p, t));
}
emit MemberUnitsUpdated(superToken, memberAddr, oldUnits, newUnits);
}

_handlePoolMemberNFT(memberAddr, newUnits);
// replicates GDAv1._isPool in order to eliminate unnecessary gas cost (less external calls)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a low hanging fruit of not violating DRY: Extract the storage related function as a global "free" function so that reusing is strictly inlining same code in two different contracts (gda logic or the pool logic).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you want faster 1.13 progress, removing this particular change is also okay for me. But I ask for no code quality degradation by not repeating code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok like this?
35e54ee

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) {
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 @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/ethereum-contracts/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@superfluid-finance/ethereum-contracts",
"description": " Ethereum contracts implementation for the Superfluid Protocol",
"version": "1.12.1",
"version": "1.13.0",
"dependencies": {
"@decentral.ee/web3-helpers": "0.5.3",
"@nomiclabs/hardhat-ethers": "2.2.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)));
}
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -270,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();
}

Expand Down Expand Up @@ -876,6 +882,34 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste
}
}

function testIncreaseDecreaseMemberUnits(
address member,
uint120 increaseAmount,
uint120 decreaseAmount
) public {
vm.assume(increaseAmount >= decreaseAmount);
vm.assume(member != address(0));
vm.assume(member != address(freePool));

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,
Expand Down
Loading
Loading