diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index 12b657a635..7517c30945 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -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 diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index 0583c2b7ed..c7c02135ff 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -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"; @@ -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 @@ -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 diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index 4dd9ad344f..3320273950 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -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, @@ -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; @@ -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; } @@ -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) { @@ -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 diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol index a54019225f..5b7682cbba 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPoolDeployerLibrary.sol @@ -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, diff --git a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol index e8f9c63159..d3eb2ea33b 100644 --- a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol @@ -14,6 +14,12 @@ struct PoolConfig { bool distributionFromAnyAddress; } +struct PoolERC20Metadata { + string name; + string symbol; + uint8 decimals; +} + /** * @title General Distribution Agreement interface * @author Superfluid @@ -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 diff --git a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol index b09524f561..5524318c49 100644 --- a/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/interfaces/agreements/gdav1/ISuperfluidPool.sol @@ -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 diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol index e8229a51e4..97d60cc6e3 100644 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol +++ b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol @@ -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"; diff --git a/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol index 6abe6eb76a..b289cace21 100644 --- a/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol +++ b/packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol @@ -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"; @@ -1078,9 +1079,7 @@ 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))); @@ -1088,9 +1087,33 @@ contract FoundrySuperfluidTester is Test { (, 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 @@ -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) diff --git a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol index 25335b7ba2..177130791d 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol @@ -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)); diff --git a/packages/ethereum-contracts/test/foundry/agreements/gdav1/SuperfluidPoolUpgradabilityMock.t.sol b/packages/ethereum-contracts/test/foundry/agreements/gdav1/SuperfluidPoolUpgradabilityMock.t.sol index 92f0922920..f4bea37815 100644 --- a/packages/ethereum-contracts/test/foundry/agreements/gdav1/SuperfluidPoolUpgradabilityMock.t.sol +++ b/packages/ethereum-contracts/test/foundry/agreements/gdav1/SuperfluidPoolUpgradabilityMock.t.sol @@ -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"); } }