Skip to content

Commit d199860

Browse files
authored
[ETHEREUM-CONTRACTS] add IERC20Metadata to SuperfluidPool (#2046)
1 parent d59ec69 commit d199860

File tree

10 files changed

+161
-22
lines changed

10 files changed

+161
-22
lines changed

packages/ethereum-contracts/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ 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
7+
8+
### Added
9+
- Superfluid Pools now implement `IERC20Metadata`, thus going forward have a name, symbol and decimals
10+
- `ISuperfluidPool.createPoolWithCustomERC20Metadata` for creating pools with custom ERC20 metadata
11+
612
## [v1.12.0]
713

814
### Added

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

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import { poolIndexDataToPDPoolIndex, SuperfluidPool } from "./SuperfluidPool.sol
1818
import { SuperfluidPoolDeployerLibrary } from "./SuperfluidPoolDeployerLibrary.sol";
1919
import {
2020
IGeneralDistributionAgreementV1,
21-
PoolConfig
21+
PoolConfig,
22+
PoolERC20Metadata
2223
} from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol";
2324
import { SuperfluidUpgradeableBeacon } from "../../upgradability/SuperfluidUpgradeableBeacon.sol";
2425
import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol";
@@ -265,16 +266,22 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
265266
actualAmount = uint256(Value.unwrap(actualDistributionAmount));
266267
}
267268

268-
function _createPool(ISuperfluidToken token, address admin, PoolConfig memory config)
269-
internal
270-
returns (ISuperfluidPool pool)
271-
{
269+
function _createPool(
270+
ISuperfluidToken token,
271+
address admin,
272+
PoolConfig memory config,
273+
PoolERC20Metadata memory poolERC20Metadata
274+
) internal returns (ISuperfluidPool pool) {
272275
// @note ensure if token and admin are the same that nothing funky happens with echidna
273276
if (admin == address(0)) revert GDA_NO_ZERO_ADDRESS_ADMIN();
274277
if (_isPool(token, admin)) revert GDA_ADMIN_CANNOT_BE_POOL();
275278

276279
pool = ISuperfluidPool(
277-
address(SuperfluidPoolDeployerLibrary.deploy(address(superfluidPoolBeacon), admin, token, config))
280+
address(
281+
SuperfluidPoolDeployerLibrary.deploy(
282+
address(superfluidPoolBeacon), admin, token, config, poolERC20Metadata
283+
)
284+
)
278285
);
279286

280287
// @note We utilize the storage slot for Universal Index State
@@ -298,7 +305,22 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
298305
override
299306
returns (ISuperfluidPool pool)
300307
{
301-
return _createPool(token, admin, config);
308+
return _createPool(
309+
token,
310+
admin,
311+
config,
312+
PoolERC20Metadata("", "", 0) // use defaults specified by the implementation contract
313+
);
314+
}
315+
316+
/// @inheritdoc IGeneralDistributionAgreementV1
317+
function createPoolWithCustomERC20Metadata(
318+
ISuperfluidToken token,
319+
address admin,
320+
PoolConfig memory config,
321+
PoolERC20Metadata memory poolERC20Metadata
322+
) external override returns (ISuperfluidPool pool) {
323+
return _createPool(token, admin, config, poolERC20Metadata);
302324
}
303325

304326
/// @inheritdoc IGeneralDistributionAgreementV1

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pragma solidity ^0.8.23;
44

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

84+
// Constants & Immutables
85+
86+
string internal constant _DEFAULT_ERC20_NAME = "Superfluid Pool";
87+
string internal constant _DEFAULT_ERC20_SYMBOL = "POOL";
88+
// ERC20 decimals implicitly defaults to 0
8489

8590
GeneralDistributionAgreementV1 public immutable GDA;
8691

92+
// State variables - NEVER REORDER!
93+
8794
ISuperfluidToken public superToken;
8895
address public admin;
8996
PoolIndexData internal _index;
@@ -101,6 +108,11 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable {
101108
/// @inheritdoc ISuperfluidPool
102109
bool public distributionFromAnyAddress;
103110

111+
// ERC20 metadata
112+
string internal _erc20Name;
113+
string internal _erc20Symbol;
114+
uint8 internal _erc20Decimals;
115+
104116
constructor(GeneralDistributionAgreementV1 gda) {
105117
GDA = gda;
106118
}
@@ -109,12 +121,18 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable {
109121
address admin_,
110122
ISuperfluidToken superToken_,
111123
bool transferabilityForUnitsOwner_,
112-
bool distributionFromAnyAddress_
124+
bool distributionFromAnyAddress_,
125+
string memory erc20Name_,
126+
string memory erc20Symbol_,
127+
uint8 erc20Decimals_
113128
) external initializer {
114129
admin = admin_;
115130
superToken = superToken_;
116131
transferabilityForUnitsOwner = transferabilityForUnitsOwner_;
117132
distributionFromAnyAddress = distributionFromAnyAddress_;
133+
_erc20Name = erc20Name_;
134+
_erc20Symbol = erc20Symbol_;
135+
_erc20Decimals = erc20Decimals_;
118136
}
119137

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

305+
/// @inheritdoc IERC20Metadata
306+
function name() external view override returns (string memory) {
307+
return bytes(_erc20Name).length == 0 ? "Superfluid Pool" : _erc20Name;
308+
}
309+
310+
/// @inheritdoc IERC20Metadata
311+
function symbol() external view override returns (string memory) {
312+
return bytes(_erc20Symbol).length == 0 ? "POOL" : _erc20Symbol;
313+
}
314+
315+
/// @inheritdoc IERC20Metadata
316+
function decimals() external view override returns (uint8) {
317+
return _erc20Decimals;
318+
}
319+
287320
function _pdPoolIndexToPoolIndexData(PDPoolIndex memory pdPoolIndex)
288321
internal
289322
pure

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,25 @@ pragma solidity ^0.8.23;
44
import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
55
import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol";
66
import { SuperfluidPool } from "./SuperfluidPool.sol";
7-
import { PoolConfig } from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol";
7+
import { PoolConfig, PoolERC20Metadata } from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol";
88

99
library SuperfluidPoolDeployerLibrary {
1010
function deploy(
1111
address beacon,
1212
address admin,
1313
ISuperfluidToken token,
14-
PoolConfig memory config
14+
PoolConfig memory config,
15+
PoolERC20Metadata memory poolERC20Metadata
1516
) external returns (SuperfluidPool pool) {
1617
bytes memory initializeCallData = abi.encodeWithSelector(
1718
SuperfluidPool.initialize.selector,
1819
admin,
1920
token,
2021
config.transferabilityForUnitsOwner,
21-
config.distributionFromAnyAddress
22+
config.distributionFromAnyAddress,
23+
poolERC20Metadata.name,
24+
poolERC20Metadata.symbol,
25+
poolERC20Metadata.decimals
2226
);
2327
BeaconProxy superfluidPoolBeaconProxy = new BeaconProxy(
2428
beacon,

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ struct PoolConfig {
1414
bool distributionFromAnyAddress;
1515
}
1616

17+
struct PoolERC20Metadata {
18+
string name;
19+
string symbol;
20+
uint8 decimals;
21+
}
22+
1723
/**
1824
* @title General Distribution Agreement interface
1925
* @author Superfluid
@@ -178,6 +184,19 @@ abstract contract IGeneralDistributionAgreementV1 is ISuperAgreement {
178184
virtual
179185
returns (ISuperfluidPool pool);
180186

187+
/// @notice Creates a new pool for `token` with custom ERC20 metadata.
188+
/// @param token The token address
189+
/// @param admin The admin of the pool
190+
/// @param poolConfig The pool configuration (see PoolConfig struct)
191+
/// @param poolERC20Metadata The pool ERC20 metadata (see PoolERC20Metadata struct)
192+
/// @return pool The pool address
193+
function createPoolWithCustomERC20Metadata(
194+
ISuperfluidToken token,
195+
address admin,
196+
PoolConfig memory poolConfig,
197+
PoolERC20Metadata memory poolERC20Metadata
198+
) external virtual returns (ISuperfluidPool pool);
199+
181200
function updateMemberUnits(ISuperfluidPool pool, address memberAddress, uint128 newUnits, bytes calldata ctx)
182201
external
183202
virtual

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// SPDX-License-Identifier: AGPLv3
22
pragma solidity >=0.8.4;
33

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

77
/**
88
* @dev The interface for any super token pool regardless of the distribution schemes.
99
*/
10-
interface ISuperfluidPool is IERC20 {
10+
interface ISuperfluidPool is IERC20, IERC20Metadata {
1111
// Custom Errors
1212

1313
error SUPERFLUID_POOL_INVALID_TIME(); // 0x83c35016

packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ import { IPoolMemberNFT } from "../agreements/gdav1/IPoolMemberNFT.sol";
2929
import { ISuperAgreement } from "./ISuperAgreement.sol";
3030
import { IConstantFlowAgreementV1 } from "../agreements/IConstantFlowAgreementV1.sol";
3131
import { IInstantDistributionAgreementV1 } from "../agreements/IInstantDistributionAgreementV1.sol";
32-
import { IGeneralDistributionAgreementV1, PoolConfig } from "../agreements/gdav1/IGeneralDistributionAgreementV1.sol";
32+
import {
33+
IGeneralDistributionAgreementV1,
34+
PoolConfig,
35+
PoolERC20Metadata
36+
} from "../agreements/gdav1/IGeneralDistributionAgreementV1.sol";
3337
import { ISuperfluidPool } from "../agreements/gdav1/ISuperfluidPool.sol";
3438
/// Superfluid App interfaces:
3539
import { ISuperApp } from "./ISuperApp.sol";

packages/ethereum-contracts/test/foundry/FoundrySuperfluidTester.t.sol

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import { Superfluid } from "../../contracts/superfluid/Superfluid.sol";
1111
import { ISuperfluidPool, SuperfluidPool } from "../../contracts/agreements/gdav1/SuperfluidPool.sol";
1212
import {
1313
IGeneralDistributionAgreementV1,
14-
PoolConfig
14+
PoolConfig,
15+
PoolERC20Metadata
1516
} from "../../contracts/interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol";
1617
import { IPoolNFTBase } from "../../contracts/interfaces/agreements/gdav1/IPoolNFTBase.sol";
1718
import { IPoolAdminNFT } from "../../contracts/interfaces/agreements/gdav1/IPoolAdminNFT.sol";
@@ -1078,19 +1079,41 @@ contract FoundrySuperfluidTester is Test {
10781079
address _poolAdmin,
10791080
bool _useForwarder,
10801081
PoolConfig memory _poolConfig
1081-
) internal returns (ISuperfluidPool) {
1082-
ISuperfluidPool localPool;
1083-
1082+
) internal returns (ISuperfluidPool localPool) {
10841083
vm.startPrank(_caller);
10851084
if (!_useForwarder) {
10861085
localPool = SuperfluidPool(address(sf.gda.createPool(_superToken, _poolAdmin, _poolConfig)));
10871086
} else {
10881087
(, localPool) = sf.gdaV1Forwarder.createPool(_superToken, _poolAdmin, _poolConfig);
10891088
}
10901089
vm.stopPrank();
1091-
_addAccount(address(localPool));
1090+
_assertPoolCreation(localPool, _useForwarder, _superToken, _poolAdmin, false, PoolERC20Metadata("", "", 0));
1091+
}
1092+
1093+
function _helperCreatePoolWithCustomERC20Metadata(
1094+
ISuperToken _superToken,
1095+
address _caller,
1096+
address _poolAdmin,
1097+
PoolConfig memory _poolConfig,
1098+
PoolERC20Metadata memory _poolERC20Metadata
1099+
) internal returns (ISuperfluidPool localPool) {
1100+
vm.startPrank(_caller);
1101+
localPool = SuperfluidPool(address(sf.gda.createPoolWithCustomERC20Metadata(
1102+
_superToken, _poolAdmin, _poolConfig, _poolERC20Metadata)));
1103+
vm.stopPrank();
1104+
_assertPoolCreation(localPool, false, _superToken, _poolAdmin, true, _poolERC20Metadata);
1105+
}
10921106

1093-
// Assert Pool Creation was properly handled
1107+
// Assert Pool Creation was properly handled
1108+
function _assertPoolCreation(
1109+
ISuperfluidPool localPool,
1110+
bool _useForwarder,
1111+
ISuperToken _superToken,
1112+
address _poolAdmin,
1113+
bool _useCustomERC20Metadata,
1114+
PoolERC20Metadata memory _poolERC20Metadata
1115+
) private {
1116+
_addAccount(address(localPool));
10941117
address poolAdmin = localPool.admin();
10951118
{
10961119
bool isPool = _useForwarder
@@ -1124,7 +1147,19 @@ contract FoundrySuperfluidTester is Test {
11241147
assertEq(poolAdmin, adjustmentFlowRecipient, "_helperCreatePool: Incorrect pool adjustment flow receiver");
11251148
}
11261149

1127-
return localPool;
1150+
// Assert ERC20 Metadata as expected
1151+
{
1152+
if (_useCustomERC20Metadata) {
1153+
assertEq(localPool.name(), _poolERC20Metadata.name, "_helperCreatePool: Pool ERC20 Metadata name mismatch");
1154+
assertEq(localPool.symbol(), _poolERC20Metadata.symbol, "_helperCreatePool: Pool ERC20 Metadata symbol mismatch");
1155+
assertEq(localPool.decimals(), _poolERC20Metadata.decimals, "_helperCreatePool: Pool ERC20 Metadata decimals mismatch");
1156+
} else {
1157+
// expect the default/fallback values hardcoded in the pool contract
1158+
assertEq(localPool.name(), "Superfluid Pool", "_helperCreatePool: Pool ERC20 Metadata name mismatch");
1159+
assertEq(localPool.symbol(), "POOL", "_helperCreatePool: Pool ERC20 Metadata symbol mismatch");
1160+
assertEq(localPool.decimals(), 0, "_helperCreatePool: Pool ERC20 Metadata decimals mismatch");
1161+
}
1162+
}
11281163
}
11291164

11301165
function _helperCreatePool(ISuperToken _superToken, address _caller, address _poolAdmin)

packages/ethereum-contracts/test/foundry/agreements/gdav1/GeneralDistributionAgreement.t.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,13 @@ contract GeneralDistributionAgreementV1IntegrationTest is FoundrySuperfluidTeste
166166
_helperCreatePool(superToken, alice, alice, useForwarder, config);
167167
}
168168

169+
function testCreatePoolWithCustomERC20Metadata(PoolConfig memory config, uint8 decimals) public {
170+
vm.assume(decimals < 32);
171+
_helperCreatePoolWithCustomERC20Metadata(
172+
superToken, alice, alice, config, PoolERC20Metadata("My SuperToken", "MYST", decimals)
173+
);
174+
}
175+
169176
function testRevertConnectPoolByNonHost(address notHost, PoolConfig memory config) public {
170177
ISuperfluidPool pool = _helperCreatePool(superToken, alice, alice, false, config);
171178
vm.assume(notHost != address(sf.host));

packages/ethereum-contracts/test/foundry/agreements/gdav1/SuperfluidPoolUpgradabilityMock.t.sol

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,14 @@ contract SuperfluidPoolStorageLayoutMock is SuperfluidPool, StorageLayoutTestBas
5050

5151
assembly { slot := distributionFromAnyAddress.slot offset := distributionFromAnyAddress.offset }
5252
if (slot != 10 || offset != 1) revert STORAGE_LOCATION_CHANGED("distributionFromAnyAddress");
53+
54+
assembly { slot := _erc20Name.slot offset := _erc20Name.offset }
55+
if (slot != 11 || offset != 0) revert STORAGE_LOCATION_CHANGED("_erc20Name");
56+
57+
assembly { slot := _erc20Symbol.slot offset := _erc20Symbol.offset }
58+
if (slot != 12 || offset != 0) revert STORAGE_LOCATION_CHANGED("_erc20Symbol");
59+
60+
assembly { slot := _erc20Decimals.slot offset := _erc20Decimals.offset }
61+
if (slot != 13 || offset != 0) revert STORAGE_LOCATION_CHANGED("_erc20Decimals");
5362
}
5463
}

0 commit comments

Comments
 (0)