Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 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 { 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,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 @@ -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
Loading
Loading