Skip to content

Commit 2081c49

Browse files
d10rhellwolf
andauthored
[ETHEREUM-CONTRACTS] allow connecting pools on behalf of members (with some constraints) (#2093)
--------- Co-authored-by: Miao ZhiCheng <miao@superfluid.finance>
1 parent 7215c9e commit 2081c49

21 files changed

+495
-121
lines changed

packages/automation-contracts/autowrap/foundry.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ root = '../../../'
33
libs = ['lib']
44
src = 'packages/automation-contracts/autowrap'
55
solc_version = "0.8.30"
6-
evm_version = 'paris'
6+
evm_version = 'shanghai'
77
optimizer = true
88
optimizer_runs = 200
99
remappings = [

packages/automation-contracts/scheduler/foundry.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ root = '../../../'
33
libs = ['lib']
44
src = 'packages/automation-contracts/scheduler'
55
solc_version = "0.8.30"
6-
evm_version = 'paris'
6+
evm_version = 'shanghai'
77
optimizer = true
88
optimizer_runs = 200
99
remappings = [

packages/ethereum-contracts/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
55

66
## [UNRELEASED]
77

8+
### Added
9+
- GDA _autoconnect_ feature: now any account can connect pool members using `tryConnectPoolFor()` as long as they have less than 4 connection slots occupied for that Super Token. This allows for smoother onboarding of new users, allowing Apps to make sure tokens distributed via GDA immediately show up in user's wallets. Accounts can opt out of this by using `setConnectPermission()`, this is mainly supposed to be used by contracts.
10+
811
### Changed
912
- Refactored `GeneralDistributionAgreementV1`: extracted functionality which reads/writes agreement data from/to the token contract into dedicated libraries:
1013
- `GDAv1StorageLib` contains data structures and related encoders/decoders.
1114
- `GDAv1StorageReader` contains getters reading agreement data from the token contract, allowing contracts to get this data without making a call to the GDA contract.
1215
- `GDAv1StorageWriter` contains functions for writing agreement data to the token contract. This can only be used by the GDA contract itself.
1316
- bump solc to "0.8.30".
17+
- Changed EVM target from `paris` to `shanghai` because now all networks with supported Superfluid deployment support it.
1418

1519
### Fixed
1620
- `ISuperfluidPool`: `getClaimable` and `getClaimableNow` could previously return non-zero values for connected pools, which was inconsistent with what `claimAll` would actually do in this situation (claim nothing).

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

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

55
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
66

7-
import { ISuperfluid, ISuperfluidGovernance } from "../../interfaces/superfluid/ISuperfluid.sol";
7+
import { ISuperfluid, ISuperfluidGovernance, IAccessControl } from "../../interfaces/superfluid/ISuperfluid.sol";
88
import {
99
BasicParticle,
1010
PDPoolIndex,
@@ -23,8 +23,6 @@ import {
2323
} from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol";
2424
import { SuperfluidUpgradeableBeacon } from "../../upgradability/SuperfluidUpgradeableBeacon.sol";
2525
import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol";
26-
import { ISuperToken } from "../../interfaces/superfluid/ISuperToken.sol";
27-
import { IPoolAdminNFT } from "../../interfaces/agreements/gdav1/IPoolAdminNFT.sol";
2826
import { ISuperfluidPool } from "../../interfaces/agreements/gdav1/ISuperfluidPool.sol";
2927
import { SlotsBitmapLibrary } from "../../libs/SlotsBitmapLibrary.sol";
3028
import { SolvencyHelperLibrary } from "../../libs/SolvencyHelperLibrary.sol";
@@ -48,6 +46,12 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
4846

4947
address public constant SUPERFLUID_POOL_DEPLOYER_ADDRESS = address(SuperfluidPoolDeployerLibrary);
5048

49+
// @dev The max number of slots which can be used for connecting pools on behalf of a member (per token)
50+
uint32 public constant MAX_POOL_AUTO_CONNECT_SLOTS = 4;
51+
52+
// @dev The ACL role owned by this contract, used to persist autoconnect permissions for accounts
53+
bytes32 constant public ACL_POOL_CONNECT_EXCLUSIVE_ROLE = keccak256("ACL_POOL_CONNECT_EXCLUSIVE_ROLE");
54+
5155
/// @dev Pool member state slot id for storing subs bitmap
5256
uint256 private constant _POOL_SUBS_BITMAP_STATE_SLOT_ID = 1;
5357
/// @dev Pool member state slot id starting point for pool connections
@@ -228,7 +232,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
228232
function _createPool(
229233
ISuperfluidToken token,
230234
address admin,
231-
PoolConfig memory config,
235+
PoolConfig calldata config,
232236
PoolERC20Metadata memory poolERC20Metadata
233237
) internal returns (ISuperfluidPool pool) {
234238
// @note ensure if token and admin are the same that nothing funky happens with echidna
@@ -245,17 +249,13 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
245249

246250
token.setIsPoolFlag(pool);
247251

248-
IPoolAdminNFT poolAdminNFT = IPoolAdminNFT(_getPoolAdminNFTAddress(token));
249-
250-
if (address(poolAdminNFT) != address(0)) {
251-
poolAdminNFT.mint(address(pool));
252-
}
252+
SuperfluidPoolDeployerLibrary.mintPoolAdminNFT(token, pool);
253253

254254
emit PoolCreated(token, admin, pool);
255255
}
256256

257257
/// @inheritdoc IGeneralDistributionAgreementV1
258-
function createPool(ISuperfluidToken token, address admin, PoolConfig memory config)
258+
function createPool(ISuperfluidToken token, address admin, PoolConfig calldata config)
259259
external
260260
override
261261
returns (ISuperfluidPool pool)
@@ -272,7 +272,7 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
272272
function createPoolWithCustomERC20Metadata(
273273
ISuperfluidToken token,
274274
address admin,
275-
PoolConfig memory config,
275+
PoolConfig calldata config,
276276
PoolERC20Metadata memory poolERC20Metadata
277277
) external override returns (ISuperfluidPool pool) {
278278
return _createPool(token, admin, config, poolERC20Metadata);
@@ -305,49 +305,106 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
305305
pool.claimAll(memberAddress);
306306
}
307307

308-
// @note setPoolConnection function naming
309-
function connectPool(ISuperfluidPool pool, bool doConnect, bytes calldata ctx)
310-
public
311-
returns (bytes memory newCtx)
308+
/// @inheritdoc IGeneralDistributionAgreementV1
309+
function connectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) {
310+
newCtx = ctx;
311+
_setPoolConnectionFor(pool, address(0), true /* doConnect */, ctx);
312+
}
313+
314+
/// @inheritdoc IGeneralDistributionAgreementV1
315+
function tryConnectPoolFor(ISuperfluidPool pool, address memberAddr, bytes calldata ctx)
316+
external
317+
override
318+
returns (bool success, bytes memory newCtx)
319+
{
320+
newCtx = ctx;
321+
322+
if (pool.superToken().isPool(this, memberAddr)) {
323+
revert GDA_CANNOT_CONNECT_POOL();
324+
}
325+
326+
// check if the member has opted out of autoconnect
327+
IAccessControl simpleACL = ISuperfluid(_host).getSimpleACL();
328+
if (simpleACL.hasRole(ACL_POOL_CONNECT_EXCLUSIVE_ROLE, memberAddr)) {
329+
success = false;
330+
} else {
331+
success = _setPoolConnectionFor(pool, memberAddr, true /* doConnect */, ctx);
332+
}
333+
}
334+
335+
function setConnectPermission(bool allow) external override {
336+
IAccessControl simpleACL = ISuperfluid(_host).getSimpleACL();
337+
if (!allow) {
338+
simpleACL.grantRole(ACL_POOL_CONNECT_EXCLUSIVE_ROLE, msg.sender);
339+
} else {
340+
simpleACL.revokeRole(ACL_POOL_CONNECT_EXCLUSIVE_ROLE, msg.sender);
341+
}
342+
}
343+
344+
/// @inheritdoc IGeneralDistributionAgreementV1
345+
function disconnectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) {
346+
newCtx = ctx;
347+
_setPoolConnectionFor(pool, address(0), false /* doConnect */, ctx);
348+
}
349+
350+
// @note memberAddr has override semantics - if set to address(0), it will be set to the msgSender
351+
function _setPoolConnectionFor(
352+
ISuperfluidPool pool,
353+
address memberAddr,
354+
bool doConnect,
355+
bytes memory ctx
356+
)
357+
internal
358+
returns (bool success)
312359
{
313360
ISuperfluidToken token = pool.superToken();
314361
ISuperfluid.Context memory currentContext = AgreementLibrary.authorizeTokenAccess(token, ctx);
315-
address msgSender = currentContext.msgSender;
316-
newCtx = ctx;
317-
bool isConnected = token.isPoolMemberConnected(this, pool, msgSender);
318-
if (doConnect != isConnected) {
319-
assert(
320-
SuperfluidPool(address(pool)).operatorConnectMember(
321-
msgSender, doConnect, uint32(currentContext.timestamp)
322-
)
323-
);
324362

363+
bool autoConnectForOtherMember = false;
364+
if (memberAddr == address(0)) {
365+
memberAddr = currentContext.msgSender;
366+
} else {
367+
autoConnectForOtherMember = true;
368+
}
369+
370+
bool isConnected = token.isPoolMemberConnected(this, pool, memberAddr);
371+
372+
if (doConnect != isConnected) {
325373
if (doConnect) {
326-
uint32 poolSlotId =
327-
_findAndFillPoolConnectionsBitmap(token, msgSender, bytes32(uint256(uint160(address(pool)))));
374+
if (autoConnectForOtherMember) {
375+
// check if we're below the slot limit for autoconnect
376+
uint256 nUsedSlots = SlotsBitmapLibrary.countUsedSlots(
377+
token, memberAddr, _POOL_SUBS_BITMAP_STATE_SLOT_ID
378+
);
379+
if (nUsedSlots >= MAX_POOL_AUTO_CONNECT_SLOTS) {
380+
return false;
381+
}
382+
}
383+
384+
uint32 poolSlotId = _findAndFillPoolConnectionsBitmap(
385+
token, memberAddr, bytes32(uint256(uint160(address(pool))))
386+
);
328387

329388
token.createPoolConnectivity
330-
(msgSender, GDAv1StorageLib.PoolConnectivity({ slotId: poolSlotId, pool: pool }));
389+
(memberAddr, GDAv1StorageLib.PoolConnectivity({ slotId: poolSlotId, pool: pool }));
331390
} else {
332391
(, GDAv1StorageLib.PoolConnectivity memory poolConnectivity) =
333-
token.getPoolConnectivity(this, msgSender, pool);
334-
token.deletePoolConnectivity(msgSender, pool);
392+
token.getPoolConnectivity(this, memberAddr, pool);
393+
token.deletePoolConnectivity(memberAddr, pool);
335394

336-
_clearPoolConnectionsBitmap(token, msgSender, poolConnectivity.slotId);
395+
_clearPoolConnectionsBitmap(token, memberAddr, poolConnectivity.slotId);
337396
}
338397

339-
emit PoolConnectionUpdated(token, pool, msgSender, doConnect, currentContext.userData);
340-
}
341-
}
398+
assert(
399+
SuperfluidPool(address(pool)).operatorConnectMember(
400+
memberAddr, doConnect, uint32(currentContext.timestamp)
401+
)
402+
);
342403

343-
/// @inheritdoc IGeneralDistributionAgreementV1
344-
function connectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) {
345-
return connectPool(pool, true, ctx);
346-
}
404+
emit PoolConnectionUpdated(token, pool, memberAddr, doConnect, currentContext.userData);
405+
}
347406

348-
/// @inheritdoc IGeneralDistributionAgreementV1
349-
function disconnectPool(ISuperfluidPool pool, bytes calldata ctx) external override returns (bytes memory newCtx) {
350-
return connectPool(pool, false, ctx);
407+
return true;
351408
}
352409

353410
/// @inheritdoc IGeneralDistributionAgreementV1
@@ -533,21 +590,6 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi
533590
}
534591
}
535592

536-
function _getPoolAdminNFTAddress(ISuperfluidToken token) internal view returns (address poolAdminNFTAddress) {
537-
// solhint-disable-next-line avoid-low-level-calls
538-
(bool success, bytes memory data) =
539-
address(token).staticcall(abi.encodeWithSelector(ISuperToken.POOL_ADMIN_NFT.selector));
540-
541-
if (success) {
542-
// @note We are aware this may revert if a Custom SuperToken's
543-
// POOL_ADMIN_NFT does not return data that can be
544-
// decoded to an address. This would mean it was intentionally
545-
// done by the creator of the Custom SuperToken logic and is
546-
// fully expected to revert in that case as the author desired.
547-
poolAdminNFTAddress = abi.decode(data, (address));
548-
}
549-
}
550-
551593
function _adjustBuffer
552594
(ISuperfluidToken token,
553595
address pool,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable {
123123
ISuperfluidToken superToken_,
124124
bool transferabilityForUnitsOwner_,
125125
bool distributionFromAnyAddress_,
126-
string memory erc20Name_,
127-
string memory erc20Symbol_,
126+
string calldata erc20Name_,
127+
string calldata erc20Symbol_,
128128
uint8 erc20Decimals_
129129
) external initializer {
130130
admin = admin_;

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.so
55
import { ISuperfluidToken } from "../../interfaces/superfluid/ISuperfluidToken.sol";
66
import { SuperfluidPool } from "./SuperfluidPool.sol";
77
import { PoolConfig, PoolERC20Metadata } from "../../interfaces/agreements/gdav1/IGeneralDistributionAgreementV1.sol";
8+
import { ISuperToken } from "../../interfaces/superfluid/ISuperToken.sol";
9+
import { ISuperfluidPool } from "../../interfaces/agreements/gdav1/ISuperfluidPool.sol";
10+
import { IPoolAdminNFT } from "../../interfaces/agreements/gdav1/IPoolAdminNFT.sol";
11+
812

913
library SuperfluidPoolDeployerLibrary {
1014
function deploy(
1115
address beacon,
1216
address admin,
1317
ISuperfluidToken token,
14-
PoolConfig memory config,
15-
PoolERC20Metadata memory poolERC20Metadata
18+
PoolConfig calldata config,
19+
PoolERC20Metadata calldata poolERC20Metadata
1620
) external returns (SuperfluidPool pool) {
1721
bytes memory initializeCallData = abi.encodeWithSelector(
1822
SuperfluidPool.initialize.selector,
@@ -30,4 +34,25 @@ library SuperfluidPoolDeployerLibrary {
3034
);
3135
pool = SuperfluidPool(address(superfluidPoolBeaconProxy));
3236
}
37+
38+
// This was moved out of GeneralDistributionAgreementV1.sol to reduce the contract size.
39+
function mintPoolAdminNFT(ISuperfluidToken token, ISuperfluidPool pool) external {
40+
address poolAdminNFTAddress;
41+
// solhint-disable-next-line avoid-low-level-calls
42+
(bool success, bytes memory data) =
43+
address(token).staticcall(abi.encodeWithSelector(ISuperToken.POOL_ADMIN_NFT.selector));
44+
45+
if (success) {
46+
// @note We are aware this may revert if a Custom SuperToken's
47+
// POOL_ADMIN_NFT does not return data that can be
48+
// decoded to an address. This would mean it was intentionally
49+
// done by the creator of the Custom SuperToken logic and is
50+
// fully expected to revert in that case as the author desired.
51+
poolAdminNFTAddress = abi.decode(data, (address));
52+
}
53+
54+
if (poolAdminNFTAddress != address(0)) {
55+
IPoolAdminNFT(poolAdminNFTAddress).mint(address(pool));
56+
}
57+
}
3358
}

packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity >= 0.8.11;
33

4-
import { ISuperfluid, ISuperToken, ISuperApp, SuperAppDefinitions } from "../interfaces/superfluid/ISuperfluid.sol";
4+
import {
5+
ISuperfluid,
6+
ISuperToken,
7+
ISuperApp,
8+
SuperAppDefinitions,
9+
IGeneralDistributionAgreementV1
10+
} from "../interfaces/superfluid/ISuperfluid.sol";
511
import { SuperTokenV1Library } from "./SuperTokenV1Library.sol";
612

713
/**
@@ -39,6 +45,16 @@ abstract contract CFASuperAppBase is ISuperApp {
3945
*/
4046
constructor(ISuperfluid host_) {
4147
HOST = host_;
48+
49+
// disable autoconnect for GDA pools
50+
IGeneralDistributionAgreementV1 gda = IGeneralDistributionAgreementV1(
51+
address(
52+
ISuperfluid(host_).getAgreementClass(
53+
keccak256("org.superfluid-finance.agreements.GeneralDistributionAgreement.v1")
54+
)
55+
)
56+
);
57+
gda.setConnectPermission(false);
4258
}
4359

4460
/**

0 commit comments

Comments
 (0)