Skip to content

Commit

Permalink
[ETHEREUM-CONTRACTS] add IERC20Metadata to SuperfluidPool (#2046)
Browse files Browse the repository at this point in the history
  • Loading branch information
d10r authored Dec 27, 2024
1 parent d59ec69 commit d199860
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 22 deletions.
6 changes: 6 additions & 0 deletions packages/ethereum-contracts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ 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

### Added
- Superfluid Pools now implement `IERC20Metadata`, thus going forward have a name, symbol and decimals
- `ISuperfluidPool.createPoolWithCustomERC20Metadata` for creating pools with custom ERC20 metadata

## [v1.12.0]

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import { poolIndexDataToPDPoolIndex, SuperfluidPool } from "./SuperfluidPool.sol
import { SuperfluidPoolDeployerLibrary } from "./SuperfluidPoolDeployerLibrary.sol";
import {
IGeneralDistributionAgreementV1,
PoolConfig
PoolConfig,
PoolERC20Metadata
} from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol";
import { SuperfluidUpgradeableBeacon } from "../../upgradability/SuperfluidUpgradeableBeacon.sol";
import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol";
Expand Down Expand Up @@ -265,16 +266,22 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
actualAmount = uint256(Value.unwrap(actualDistributionAmount));
}

function _createPool(ISuperfluidToken token, address admin, PoolConfig memory config)
internal
returns (ISuperfluidPool pool)
{
function _createPool(
ISuperfluidToken token,
address admin,
PoolConfig memory config,
PoolERC20Metadata memory poolERC20Metadata
) 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();

pool = ISuperfluidPool(
address(SuperfluidPoolDeployerLibrary.deploy(address(superfluidPoolBeacon), admin, token, config))
address(
SuperfluidPoolDeployerLibrary.deploy(
address(superfluidPoolBeacon), admin, token, config, poolERC20Metadata
)
)
);

// @note We utilize the storage slot for Universal Index State
Expand All @@ -298,7 +305,22 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
override
returns (ISuperfluidPool pool)
{
return _createPool(token, admin, config);
return _createPool(
token,
admin,
config,
PoolERC20Metadata("", "", 0) // use defaults specified by the implementation contract
);
}

/// @inheritdoc IGeneralDistributionAgreementV1
function createPoolWithCustomERC20Metadata(
ISuperfluidToken token,
address admin,
PoolConfig memory config,
PoolERC20Metadata memory poolERC20Metadata
) external override returns (ISuperfluidPool pool) {
return _createPool(token, admin, config, poolERC20Metadata);
}

/// @inheritdoc IGeneralDistributionAgreementV1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity ^0.8.23;

// Notes: We use these interfaces in natspec documentation below, grep @inheritdoc
// solhint-disable-next-line no-unused-import
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {
BasicParticle,
Expand Down Expand Up @@ -81,9 +81,16 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable {
int256 claimedValue;
}

// Constants & Immutables

string internal constant _DEFAULT_ERC20_NAME = "Superfluid Pool";
string internal constant _DEFAULT_ERC20_SYMBOL = "POOL";
// ERC20 decimals implicitly defaults to 0

GeneralDistributionAgreementV1 public immutable GDA;

// State variables - NEVER REORDER!

ISuperfluidToken public superToken;
address public admin;
PoolIndexData internal _index;
Expand All @@ -101,6 +108,11 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable {
/// @inheritdoc ISuperfluidPool
bool public distributionFromAnyAddress;

// ERC20 metadata
string internal _erc20Name;
string internal _erc20Symbol;
uint8 internal _erc20Decimals;

constructor(GeneralDistributionAgreementV1 gda) {
GDA = gda;
}
Expand All @@ -109,12 +121,18 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable {
address admin_,
ISuperfluidToken superToken_,
bool transferabilityForUnitsOwner_,
bool distributionFromAnyAddress_
bool distributionFromAnyAddress_,
string memory erc20Name_,
string memory erc20Symbol_,
uint8 erc20Decimals_
) external initializer {
admin = admin_;
superToken = superToken_;
transferabilityForUnitsOwner = transferabilityForUnitsOwner_;
distributionFromAnyAddress = distributionFromAnyAddress_;
_erc20Name = erc20Name_;
_erc20Symbol = erc20Symbol_;
_erc20Decimals = erc20Decimals_;
}

function proxiableUUID() public pure override returns (bytes32) {
Expand Down Expand Up @@ -284,6 +302,21 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable {
else return (_index.wrappedFlowRate * uint256(units).toInt256()).toInt96();
}

/// @inheritdoc IERC20Metadata
function name() external view override returns (string memory) {
return bytes(_erc20Name).length == 0 ? "Superfluid Pool" : _erc20Name;
}

/// @inheritdoc IERC20Metadata
function symbol() external view override returns (string memory) {
return bytes(_erc20Symbol).length == 0 ? "POOL" : _erc20Symbol;
}

/// @inheritdoc IERC20Metadata
function decimals() external view override returns (uint8) {
return _erc20Decimals;
}

function _pdPoolIndexToPoolIndexData(PDPoolIndex memory pdPoolIndex)
internal
pure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@ pragma solidity ^0.8.23;
import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol";
import { SuperfluidPool } from "./SuperfluidPool.sol";
import { PoolConfig } from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol";
import { PoolConfig, PoolERC20Metadata } from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol";

library SuperfluidPoolDeployerLibrary {
function deploy(
address beacon,
address admin,
ISuperfluidToken token,
PoolConfig memory config
PoolConfig memory config,
PoolERC20Metadata memory poolERC20Metadata
) external returns (SuperfluidPool pool) {
bytes memory initializeCallData = abi.encodeWithSelector(
SuperfluidPool.initialize.selector,
admin,
token,
config.transferabilityForUnitsOwner,
config.distributionFromAnyAddress
config.distributionFromAnyAddress,
poolERC20Metadata.name,
poolERC20Metadata.symbol,
poolERC20Metadata.decimals
);
BeaconProxy superfluidPoolBeaconProxy = new BeaconProxy(
beacon,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ struct PoolConfig {
bool distributionFromAnyAddress;
}

struct PoolERC20Metadata {
string name;
string symbol;
uint8 decimals;
}

/**
* @title General Distribution Agreement interface
* @author Superfluid
Expand Down Expand Up @@ -178,6 +184,19 @@ abstract contract IGeneralDistributionAgreementV1 is ISuperAgreement {
virtual
returns (ISuperfluidPool pool);

/// @notice Creates a new pool for `token` with custom ERC20 metadata.
/// @param token The token address
/// @param admin The admin of the pool
/// @param poolConfig The pool configuration (see PoolConfig struct)
/// @param poolERC20Metadata The pool ERC20 metadata (see PoolERC20Metadata struct)
/// @return pool The pool address
function createPoolWithCustomERC20Metadata(
ISuperfluidToken token,
address admin,
PoolConfig memory poolConfig,
PoolERC20Metadata memory poolERC20Metadata
) external virtual returns (ISuperfluidPool pool);

function updateMemberUnits(ISuperfluidPool pool, address memberAddress, uint128 newUnits, bytes calldata ctx)
external
virtual
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// SPDX-License-Identifier: AGPLv3
pragma solidity >=0.8.4;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { ISuperfluidToken } from "../../superfluid/ISuperfluidToken.sol";

/**
* @dev The interface for any super token pool regardless of the distribution schemes.
*/
interface ISuperfluidPool is IERC20 {
interface ISuperfluidPool is IERC20, IERC20Metadata {
// Custom Errors

error SUPERFLUID_POOL_INVALID_TIME(); // 0x83c35016
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ 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 } from "../agreements/gdav1/IGeneralDistributionAgreementV1.sol";
import {
IGeneralDistributionAgreementV1,
PoolConfig,
PoolERC20Metadata
} from "../agreements/gdav1/IGeneralDistributionAgreementV1.sol";
import { ISuperfluidPool } from "../agreements/gdav1/ISuperfluidPool.sol";
/// Superfluid App interfaces:
import { ISuperApp } from "./ISuperApp.sol";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { Superfluid } from "../../contracts/superfluid/Superfluid.sol";
import { ISuperfluidPool, SuperfluidPool } from "../../contracts/agreements/gdav1/SuperfluidPool.sol";
import {
IGeneralDistributionAgreementV1,
PoolConfig
PoolConfig,
PoolERC20Metadata
} 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";
Expand Down Expand Up @@ -1078,19 +1079,41 @@ contract FoundrySuperfluidTester is Test {
address _poolAdmin,
bool _useForwarder,
PoolConfig memory _poolConfig
) internal returns (ISuperfluidPool) {
ISuperfluidPool localPool;

) internal returns (ISuperfluidPool localPool) {
vm.startPrank(_caller);
if (!_useForwarder) {
localPool = SuperfluidPool(address(sf.gda.createPool(_superToken, _poolAdmin, _poolConfig)));
} else {
(, localPool) = sf.gdaV1Forwarder.createPool(_superToken, _poolAdmin, _poolConfig);
}
vm.stopPrank();
_addAccount(address(localPool));
_assertPoolCreation(localPool, _useForwarder, _superToken, _poolAdmin, false, PoolERC20Metadata("", "", 0));
}

function _helperCreatePoolWithCustomERC20Metadata(
ISuperToken _superToken,
address _caller,
address _poolAdmin,
PoolConfig memory _poolConfig,
PoolERC20Metadata memory _poolERC20Metadata
) internal returns (ISuperfluidPool localPool) {
vm.startPrank(_caller);
localPool = SuperfluidPool(address(sf.gda.createPoolWithCustomERC20Metadata(
_superToken, _poolAdmin, _poolConfig, _poolERC20Metadata)));
vm.stopPrank();
_assertPoolCreation(localPool, false, _superToken, _poolAdmin, true, _poolERC20Metadata);
}

// Assert Pool Creation was properly handled
// Assert Pool Creation was properly handled
function _assertPoolCreation(
ISuperfluidPool localPool,
bool _useForwarder,
ISuperToken _superToken,
address _poolAdmin,
bool _useCustomERC20Metadata,
PoolERC20Metadata memory _poolERC20Metadata
) private {
_addAccount(address(localPool));
address poolAdmin = localPool.admin();
{
bool isPool = _useForwarder
Expand Down Expand Up @@ -1124,7 +1147,19 @@ contract FoundrySuperfluidTester is Test {
assertEq(poolAdmin, adjustmentFlowRecipient, "_helperCreatePool: Incorrect pool adjustment flow receiver");
}

return localPool;
// Assert ERC20 Metadata as expected
{
if (_useCustomERC20Metadata) {
assertEq(localPool.name(), _poolERC20Metadata.name, "_helperCreatePool: Pool ERC20 Metadata name mismatch");
assertEq(localPool.symbol(), _poolERC20Metadata.symbol, "_helperCreatePool: Pool ERC20 Metadata symbol mismatch");
assertEq(localPool.decimals(), _poolERC20Metadata.decimals, "_helperCreatePool: Pool ERC20 Metadata decimals mismatch");
} else {
// expect the default/fallback values hardcoded in the pool contract
assertEq(localPool.name(), "Superfluid Pool", "_helperCreatePool: Pool ERC20 Metadata name mismatch");
assertEq(localPool.symbol(), "POOL", "_helperCreatePool: Pool ERC20 Metadata symbol mismatch");
assertEq(localPool.decimals(), 0, "_helperCreatePool: Pool ERC20 Metadata decimals mismatch");
}
}
}

function _helperCreatePool(ISuperToken _superToken, address _caller, address _poolAdmin)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste
_helperCreatePool(superToken, alice, alice, useForwarder, config);
}

function testCreatePoolWithCustomERC20Metadata(PoolConfig memory config, uint8 decimals) public {
vm.assume(decimals < 32);
_helperCreatePoolWithCustomERC20Metadata(
superToken, alice, alice, config, PoolERC20Metadata("My SuperToken", "MYST", decimals)
);
}

function testRevertConnectPoolByNonHost(address notHost, PoolConfig memory config) public {
ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config);
vm.assume(notHost != address(sf.host));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,14 @@ contract SuperfluidPoolStorageLayoutMock is SuperfluidPool, StorageLayoutTestBas

assembly { slot := distributionFromAnyAddress.slot offset := distributionFromAnyAddress.offset }
if (slot != 10 || offset != 1) revert STORAGE_LOCATION_CHANGED("distributionFromAnyAddress");

assembly { slot := _erc20Name.slot offset := _erc20Name.offset }
if (slot != 11 || offset != 0) revert STORAGE_LOCATION_CHANGED("_erc20Name");

assembly { slot := _erc20Symbol.slot offset := _erc20Symbol.offset }
if (slot != 12 || offset != 0) revert STORAGE_LOCATION_CHANGED("_erc20Symbol");

assembly { slot := _erc20Decimals.slot offset := _erc20Decimals.offset }
if (slot != 13 || offset != 0) revert STORAGE_LOCATION_CHANGED("_erc20Decimals");
}
}

0 comments on commit d199860

Please sign in to comment.