From fc9735d2aad45ae827165a9993700fdc007d2aac Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 16 Oct 2025 15:39:32 +0200 Subject: [PATCH 01/11] emit Transfer event on flow create/delete and units changing from/to 0 --- .../contracts/agreements/ConstantFlowAgreementV1.sol | 7 +++++++ .../agreements/gdav1/GeneralDistributionAgreementV1.sol | 8 ++++++++ .../contracts/agreements/gdav1/SuperfluidPool.sol | 5 +++++ .../contracts/interfaces/superfluid/ISuperfluidToken.sol | 7 +++++++ .../contracts/superfluid/SuperfluidToken.sol | 4 ++++ 5 files changed, 31 insertions(+) diff --git a/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol index ec7a840200..8418d86dd6 100644 --- a/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol @@ -453,6 +453,13 @@ contract ConstantFlowAgreementV1 is ctx, currentContext); } + if ( + oldFlowData.flowRate == 0 && flowParams.flowRate != 0 || + oldFlowData.flowRate != 0 && flowParams.flowRate == 0 + ) { + flowVars.token.emitZeroTransfer(flowVars.sender, flowVars.receiver); + } + _requireAvailableBalance(flowVars.token, flowVars.sender, currentContext); } diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index ff864bbf30..91eff78a44 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -801,6 +801,14 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi return true; } + function tokenEmitZeroTransfer(ISuperfluidToken superToken, address from, address to) external { + if (superToken.isPool(this, msg.sender) == false) { + revert GDA_ONLY_SUPER_TOKEN_POOL(); + } + + superToken.emitZeroTransfer(from, to); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////// // TokenMonad interface ////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index 1a12c5edb6..c05c24788a 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -453,6 +453,11 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { _membersData[memberAddr] = _pdPoolMemberToMemberData(pdPoolMember, claimedValue); assert(GDA.appendIndexUpdateByPool(superToken, p, t)); } + + if (oldUnits == 0 && newUnits != 0 || oldUnits != 0 && newUnits == 0) { + GDA.tokenEmitZeroTransfer(superToken, memberAddr, address(0)); + } + emit MemberUnitsUpdated(superToken, memberAddr, oldUnits, newUnits); } diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluidToken.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluidToken.sol index 36e06fa123..13138fcc1e 100644 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluidToken.sol +++ b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluidToken.sol @@ -431,4 +431,11 @@ interface ISuperfluidToken { uint256 rewardAmount, uint256 bailoutAmount ); + + /** + * @dev Emit an ERC20.Transfer event with zero amount, helps indexers track token holders. + * @param from The address from which the transfer is happening + * @param to The address to which the transfer is happening + */ + function emitZeroTransfer(address from, address to) external; } diff --git a/packages/ethereum-contracts/contracts/superfluid/SuperfluidToken.sol b/packages/ethereum-contracts/contracts/superfluid/SuperfluidToken.sol index c75fc48f69..feb83de72e 100644 --- a/packages/ethereum-contracts/contracts/superfluid/SuperfluidToken.sol +++ b/packages/ethereum-contracts/contracts/superfluid/SuperfluidToken.sol @@ -376,6 +376,10 @@ abstract contract SuperfluidToken is ISuperfluidToken ); } + function emitZeroTransfer(address from, address to) external onlyAgreement { + emit IERC20.Transfer(from, to, 0); + } + /************************************************************************** * Modifiers *************************************************************************/ From 39651f9d498adb2f5ca0dba76568663f2e42e247 Mon Sep 17 00:00:00 2001 From: didi Date: Mon, 20 Oct 2025 15:58:04 +0200 Subject: [PATCH 02/11] fix node version info in README --- packages/ethereum-contracts/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ethereum-contracts/README.md b/packages/ethereum-contracts/README.md index 6a10cee92f..9330f7d6b1 100644 --- a/packages/ethereum-contracts/README.md +++ b/packages/ethereum-contracts/README.md @@ -35,7 +35,7 @@ If you're building a smart contract that uses Superfluid protocol, or even your ### Installation Prerequisites: -- [node.js v18+](https://nodejs.org/en/download). The project recommends 22, and is tested with node 18,20,22. +- [node.js v20+](https://nodejs.org/en/download). The project recommends 24, and is tested with node 20,22,24. - [yarn](https://classic.yarnpkg.com/en/docs/install) - [forge](https://book.getfoundry.sh/getting-started/installation) @@ -305,7 +305,7 @@ If you want contribute to Superfluid protocol contracts instead of just interfac ### Setup Development Environment -Prerequisites: You need node.js v18+ and yarn installed. +Prerequisites: You need node.js v20+ and yarn installed. First, check out this repository and cd into it. ```sh From 45f0d775b35913aca151435a6293d496f2c10a26 Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 22 Oct 2025 10:28:02 +0200 Subject: [PATCH 03/11] added tests --- .../agreements/ConstantFlowAgreementV1.sol | 9 +-- .../agreements/gdav1/SuperfluidPool.sol | 2 +- .../test/foundry/superfluid/SuperToken.t.sol | 61 ++++++++++++++++++- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol index 8418d86dd6..4832ec4a08 100644 --- a/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol @@ -453,12 +453,7 @@ contract ConstantFlowAgreementV1 is ctx, currentContext); } - if ( - oldFlowData.flowRate == 0 && flowParams.flowRate != 0 || - oldFlowData.flowRate != 0 && flowParams.flowRate == 0 - ) { - flowVars.token.emitZeroTransfer(flowVars.sender, flowVars.receiver); - } + flowVars.token.emitZeroTransfer(flowVars.sender, flowVars.receiver); _requireAvailableBalance(flowVars.token, flowVars.sender, currentContext); } @@ -599,6 +594,8 @@ contract ConstantFlowAgreementV1 is newCtx, currentContext); } } + + flowVars.token.emitZeroTransfer(flowVars.sender, flowVars.receiver); } /************************************************************************** diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index c05c24788a..8e559a4f0c 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -455,7 +455,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { } if (oldUnits == 0 && newUnits != 0 || oldUnits != 0 && newUnits == 0) { - GDA.tokenEmitZeroTransfer(superToken, memberAddr, address(0)); + GDA.tokenEmitZeroTransfer(superToken, address(this), memberAddr); } emit MemberUnitsUpdated(superToken, memberAddr, oldUnits, newUnits); diff --git a/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol index 2d2183df21..b1bf39f87e 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol @@ -1,18 +1,23 @@ // SPDX-License-Identifier: AGPLv3 pragma solidity ^0.8.23; -import { Test } from "forge-std/Test.sol"; +import { Test, console } from "forge-std/Test.sol"; import { UUPSProxy } from "../../../contracts/upgradability/UUPSProxy.sol"; import { UUPSProxiable } from "../../../contracts/upgradability/UUPSProxiable.sol"; -import { IERC20, ISuperToken, SuperToken } +import { IERC20, ISuperToken, ISuperfluidPool, PoolConfig } + from "../../../contracts/interfaces/superfluid/ISuperfluid.sol"; +import { SuperToken } from "../../../contracts/superfluid/SuperToken.sol"; import { PoolAdminNFT, IPoolAdminNFT } from "../../../contracts/agreements/gdav1/PoolAdminNFT.sol"; import { FoundrySuperfluidTester } from "../FoundrySuperfluidTester.t.sol"; import { TestToken } from "../../../contracts/utils/TestToken.sol"; import { TokenDeployerLibrary } from "../../../contracts/utils/SuperfluidFrameworkDeploymentSteps.t.sol"; +import { SuperTokenV1Library } from "../../../contracts/apps/SuperTokenV1Library.sol"; contract SuperTokenIntegrationTest is FoundrySuperfluidTester { - constructor() FoundrySuperfluidTester(0) { } + using SuperTokenV1Library for ISuperToken; + + constructor() FoundrySuperfluidTester(1) { } function setUp() public override { super.setUp(); @@ -254,4 +259,54 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester { assertEq(localSuperToken.nonces(permitSigner), 1, "Nonce should be incremented"); assertEq(localSuperToken.allowance(permitSigner, spender), amount, "Allowance should be set"); } + + // Verify zero Transfer events being emitted by CFA and GDA actions + function testEmitZeroTransferEvent() public { + vm.startPrank(admin); + + // case 1: create flow + vm.expectEmit(address(superToken)); + emit IERC20.Transfer(admin, alice, 0); + superToken.createFlow(alice, 1); + + // case 2: delete flow + vm.expectEmit(address(superToken)); + emit IERC20.Transfer(admin, alice, 0); + superToken.deleteFlow(admin, alice); + + // create a pool for the next tests + ISuperfluidPool pool = superToken.createPool( + admin, + PoolConfig({ + transferabilityForUnitsOwner: true, + distributionFromAnyAddress: true + }) + ); + + // case 3: assign pool units + vm.expectEmit(address(superToken)); + emit IERC20.Transfer(address(pool), alice, 0); + pool.updateMemberUnits(alice, 1); + + vm.stopPrank(); + + // case 4: test pool token transfer + vm.startPrank(alice); + // This emits 2 Transfer events, because the sender's units toggle to 0 and the receiver units from 0 + vm.expectEmit(address(superToken)); + emit IERC20.Transfer(address(pool), alice, 0); + vm.expectEmit(address(superToken)); + emit IERC20.Transfer(address(pool), bob, 0); + IERC20(pool).transfer(bob, 1); + + vm.stopPrank(); + + // case 5: remove pool units + vm.startPrank(admin); + vm.expectEmit(address(superToken)); + emit IERC20.Transfer(address(pool), bob, 0); + pool.updateMemberUnits(bob, 0); + + vm.stopPrank(); + } } From 9f3f4021edf7a49fa7f9e3bf9b064a1fabeb6667 Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 22 Oct 2025 10:32:07 +0200 Subject: [PATCH 04/11] updated CHANGELOG --- packages/ethereum-contracts/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index dbaa27f513..88ce7a652e 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -3,7 +3,7 @@ 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.14.0] ### Added - 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. @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `GDAv1StorageWriter` contains functions for writing agreement data to the token contract. This can only be used by the GDA contract itself. - bump solc to "0.8.30". - Changed EVM target from `paris` to `shanghai` because now all networks with supported Superfluid deployment support it. +- Emit ERC20 `Transfer` events (with amount 0) on CFA and GDA actions potentially leading to account balance changes. This shall help indexers to keep track of SuperToken holders and account balances. ### Fixed - `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). From 0a110a16830218d57d06dfbf772f0ae28b391d4e Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 22 Oct 2025 10:35:11 +0200 Subject: [PATCH 05/11] bumped version to 1.14.0 --- packages/automation-contracts/autowrap/package.json | 2 +- packages/automation-contracts/scheduler/package.json | 2 +- packages/ethereum-contracts/package.json | 2 +- packages/hot-fuzz/package.json | 4 ++-- packages/js-sdk/package.json | 2 +- packages/sdk-core/package.json | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/automation-contracts/autowrap/package.json b/packages/automation-contracts/autowrap/package.json index 3a96ac9998..5ceb249f05 100644 --- a/packages/automation-contracts/autowrap/package.json +++ b/packages/automation-contracts/autowrap/package.json @@ -3,7 +3,7 @@ "description": "Open contracts that allow upgrading underlying token to supertokens based on running stream", "version": "0.3.0", "devDependencies": { - "@superfluid-finance/ethereum-contracts": "^1.13.0", + "@superfluid-finance/ethereum-contracts": "^1.14.0", "@superfluid-finance/metadata": "^1.6.2" }, "license": "MIT", diff --git a/packages/automation-contracts/scheduler/package.json b/packages/automation-contracts/scheduler/package.json index 1b058ed79b..b6e7c979d9 100644 --- a/packages/automation-contracts/scheduler/package.json +++ b/packages/automation-contracts/scheduler/package.json @@ -3,7 +3,7 @@ "description": "Open contracts that allow scheduling streams and vestings onchain", "version": "1.3.0", "devDependencies": { - "@superfluid-finance/ethereum-contracts": "^1.13.0", + "@superfluid-finance/ethereum-contracts": "^1.14.0", "@superfluid-finance/metadata": "^1.6.2" }, "license": "MIT", diff --git a/packages/ethereum-contracts/package.json b/packages/ethereum-contracts/package.json index 1adc2a8ec0..7a4250b40a 100644 --- a/packages/ethereum-contracts/package.json +++ b/packages/ethereum-contracts/package.json @@ -1,7 +1,7 @@ { "name": "@superfluid-finance/ethereum-contracts", "description": " Ethereum contracts implementation for the Superfluid Protocol", - "version": "1.13.0", + "version": "1.14.0", "dependencies": { "@decentral.ee/web3-helpers": "0.5.3", "@nomiclabs/hardhat-ethers": "2.2.3", diff --git a/packages/hot-fuzz/package.json b/packages/hot-fuzz/package.json index 7037b37cbd..deb60e28f4 100644 --- a/packages/hot-fuzz/package.json +++ b/packages/hot-fuzz/package.json @@ -7,13 +7,13 @@ }, "bugs": "https://github.com/superfluid-finance/protocol-monorepo/issues", "devDependencies": { - "@superfluid-finance/ethereum-contracts": "^1.13.0" + "@superfluid-finance/ethereum-contracts": "^1.14.0" }, "homepage": "https://github.com/superfluid-finance/protocol-monorepo#readme", "license": "AGPL-3.0", "main": "index.js", "peerDependencies": { - "@superfluid-finance/ethereum-contracts": "1.13.0" + "@superfluid-finance/ethereum-contracts": "1.14.0" }, "repository": { "type": "git", diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index a1e6869023..395138a7b9 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -13,7 +13,7 @@ "node-fetch": "2.7.0" }, "devDependencies": { - "@superfluid-finance/ethereum-contracts": "^1.13.0", + "@superfluid-finance/ethereum-contracts": "^1.14.0", "chai-as-promised": "^8.0.0", "webpack": "^5.94.0", "webpack-bundle-analyzer": "^4.10.2", diff --git a/packages/sdk-core/package.json b/packages/sdk-core/package.json index 6970684c88..12f4b6f2a7 100644 --- a/packages/sdk-core/package.json +++ b/packages/sdk-core/package.json @@ -4,7 +4,7 @@ "version": "0.9.0", "bugs": "https://github.com/superfluid-finance/protocol-monorepo/issues", "dependencies": { - "@superfluid-finance/ethereum-contracts": "1.13.0", + "@superfluid-finance/ethereum-contracts": "1.14.0", "@superfluid-finance/metadata": "^1.6.2", "graphql-request": "6.1.0", "lodash": "4.17.21", From 45421298b5e65a3bbb9f0c4e742e77129b281038 Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 22 Oct 2025 11:39:49 +0200 Subject: [PATCH 06/11] don't emit Approval event on transferFrom, refs #2115 --- .../contracts/superfluid/SuperToken.sol | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol b/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol index fa9d4af9ad..a06e355bc9 100644 --- a/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol +++ b/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol @@ -360,7 +360,7 @@ contract SuperToken is if (spender != holder) { require(amount <= _allowances[holder][spender], "SuperToken: transfer amount exceeds allowance"); // TODO: this triggers an `Approval` event, which shouldn't happen for transfers. - _approve(holder, spender, _allowances[holder][spender] - amount); + _approve(holder, spender, _allowances[holder][spender] - amount, false); } return true; @@ -504,7 +504,17 @@ contract SuperToken is * - `account` cannot be the zero address. * - `spender` cannot be the zero address. */ - function _approve(address account, address spender, uint256 amount) + function _approve(address owner, address spender, uint256 value) internal { + _approve(owner, spender, value, true); + } + + /** + * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event. + * + * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made + * during the `transferFrom` operation set the flag to false. + */ + function _approve(address account, address spender, uint256 amount, bool emitEvent) internal { if (account == address(0)) { @@ -515,7 +525,10 @@ contract SuperToken is } _allowances[account][spender] = amount; - emit Approval(account, spender, amount); + + if (emitEvent) { + emit Approval(account, spender, amount); + } } /** From a74be69e7ee0d7e493dfbde472fb6d8f7a7ac7de Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 22 Oct 2025 11:48:00 +0200 Subject: [PATCH 07/11] renamed to PseudoTransfer --- .../contracts/agreements/ConstantFlowAgreementV1.sol | 4 ++-- .../agreements/gdav1/GeneralDistributionAgreementV1.sol | 4 ++-- .../contracts/agreements/gdav1/SuperfluidPool.sol | 2 +- .../contracts/interfaces/superfluid/ISuperfluidToken.sol | 2 +- .../contracts/superfluid/SuperfluidToken.sol | 2 +- .../test/foundry/superfluid/SuperToken.t.sol | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol index 4832ec4a08..0daabe2234 100644 --- a/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol @@ -453,7 +453,7 @@ contract ConstantFlowAgreementV1 is ctx, currentContext); } - flowVars.token.emitZeroTransfer(flowVars.sender, flowVars.receiver); + flowVars.token.emitPseudoTransfer(flowVars.sender, flowVars.receiver); _requireAvailableBalance(flowVars.token, flowVars.sender, currentContext); } @@ -595,7 +595,7 @@ contract ConstantFlowAgreementV1 is } } - flowVars.token.emitZeroTransfer(flowVars.sender, flowVars.receiver); + flowVars.token.emitPseudoTransfer(flowVars.sender, flowVars.receiver); } /************************************************************************** diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol index 91eff78a44..a9e762d17c 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/GeneralDistributionAgreementV1.sol @@ -801,12 +801,12 @@ contract GeneralDistributionAgreementV1 is AgreementBase, TokenMonad, IGeneralDi return true; } - function tokenEmitZeroTransfer(ISuperfluidToken superToken, address from, address to) external { + function tokenEmitPseudoTransfer(ISuperfluidToken superToken, address from, address to) external { if (superToken.isPool(this, msg.sender) == false) { revert GDA_ONLY_SUPER_TOKEN_POOL(); } - superToken.emitZeroTransfer(from, to); + superToken.emitPseudoTransfer(from, to); } ////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index 8e559a4f0c..863ce6a966 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -455,7 +455,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { } if (oldUnits == 0 && newUnits != 0 || oldUnits != 0 && newUnits == 0) { - GDA.tokenEmitZeroTransfer(superToken, address(this), memberAddr); + GDA.tokenEmitPseudoTransfer(superToken, address(this), memberAddr); } emit MemberUnitsUpdated(superToken, memberAddr, oldUnits, newUnits); diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluidToken.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluidToken.sol index 13138fcc1e..d9bcd86e61 100644 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluidToken.sol +++ b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluidToken.sol @@ -437,5 +437,5 @@ interface ISuperfluidToken { * @param from The address from which the transfer is happening * @param to The address to which the transfer is happening */ - function emitZeroTransfer(address from, address to) external; + function emitPseudoTransfer(address from, address to) external; } diff --git a/packages/ethereum-contracts/contracts/superfluid/SuperfluidToken.sol b/packages/ethereum-contracts/contracts/superfluid/SuperfluidToken.sol index feb83de72e..f2c3c6d8f4 100644 --- a/packages/ethereum-contracts/contracts/superfluid/SuperfluidToken.sol +++ b/packages/ethereum-contracts/contracts/superfluid/SuperfluidToken.sol @@ -376,7 +376,7 @@ abstract contract SuperfluidToken is ISuperfluidToken ); } - function emitZeroTransfer(address from, address to) external onlyAgreement { + function emitPseudoTransfer(address from, address to) external onlyAgreement { emit IERC20.Transfer(from, to, 0); } diff --git a/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol index b1bf39f87e..4ce9d93d01 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol @@ -261,7 +261,7 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester { } // Verify zero Transfer events being emitted by CFA and GDA actions - function testEmitZeroTransferEvent() public { + function testEmitPseudoTransferEvent() public { vm.startPrank(admin); // case 1: create flow From cc4a064e5e1459ea60d88e275e412d5b0e849f96 Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 22 Oct 2025 11:55:38 +0200 Subject: [PATCH 08/11] adjust test --- .../test/contracts/superfluid/ERC20.behavior.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/packages/ethereum-contracts/test/contracts/superfluid/ERC20.behavior.ts b/packages/ethereum-contracts/test/contracts/superfluid/ERC20.behavior.ts index ecb43e0bd5..c428fbc43b 100644 --- a/packages/ethereum-contracts/test/contracts/superfluid/ERC20.behavior.ts +++ b/packages/ethereum-contracts/test/contracts/superfluid/ERC20.behavior.ts @@ -132,23 +132,6 @@ export function shouldBehaveLikeERC20( .to.emit(this.token, "Transfer") .withArgs(tokenOwner, to, amount); }); - - it("emits an approval event", async function () { - await expect( - this.token - .connect(spenderSigner) - .transferFrom(tokenOwner, to, amount) - ) - .to.emit(this.token, "Approval") - .withArgs( - tokenOwner, - spender, - await this.token.allowance( - tokenOwner, - spender - ) - ); - }); }); describe("when the token owner does not have enough balance", function () { From 61392d4405d698402dd50976e24d77046c5a979f Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 22 Oct 2025 12:00:05 +0200 Subject: [PATCH 09/11] being pedantic --- .../contracts/agreements/gdav1/SuperfluidPool.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol index 863ce6a966..1a36d38752 100644 --- a/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol +++ b/packages/ethereum-contracts/contracts/agreements/gdav1/SuperfluidPool.sol @@ -454,7 +454,7 @@ contract SuperfluidPool is ISuperfluidPool, BeaconProxiable { assert(GDA.appendIndexUpdateByPool(superToken, p, t)); } - if (oldUnits == 0 && newUnits != 0 || oldUnits != 0 && newUnits == 0) { + if ((oldUnits == 0 || newUnits == 0) && oldUnits != newUnits) { GDA.tokenEmitPseudoTransfer(superToken, address(this), memberAddr); } From 24e96a3d5ba0bd019b4df210482cb78eb78000b0 Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 22 Oct 2025 12:03:30 +0200 Subject: [PATCH 10/11] add comment for Approval change --- packages/ethereum-contracts/CHANGELOG.md | 1 + .../ethereum-contracts/contracts/superfluid/SuperToken.sol | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index 88ce7a652e..96b857a072 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - bump solc to "0.8.30". - Changed EVM target from `paris` to `shanghai` because now all networks with supported Superfluid deployment support it. - Emit ERC20 `Transfer` events (with amount 0) on CFA and GDA actions potentially leading to account balance changes. This shall help indexers to keep track of SuperToken holders and account balances. +- Don't emit ERC20 `Approval` events on `transferFrom` operations. This is consistent with the OpenZeppelin ERC20 implementation from v5 onwards. Change effective only for SuperTokens using the latest logic. ### Fixed - `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). diff --git a/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol b/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol index a06e355bc9..5f7b7d0c0f 100644 --- a/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol +++ b/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol @@ -513,6 +513,10 @@ contract SuperToken is * * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made * during the `transferFrom` operation set the flag to false. + * + * Note: In the OpenZeppelin implementation, from v5 onwards, {transferFrom} doesn't emit an {Approval} event. + * By adding this overloaded function and using it for {transferFrom}, we replicate that change, + * because it seems semantically more correct. */ function _approve(address account, address spender, uint256 amount, bool emitEvent) internal From aaafa85f4288e532381e29125d71bc36c6d3aba0 Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 22 Oct 2025 13:23:40 +0200 Subject: [PATCH 11/11] fix warning --- .../ethereum-contracts/contracts/utils/ERC1820Implementer.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ethereum-contracts/contracts/utils/ERC1820Implementer.sol b/packages/ethereum-contracts/contracts/utils/ERC1820Implementer.sol index 7138f019bb..d9137bdf5a 100644 --- a/packages/ethereum-contracts/contracts/utils/ERC1820Implementer.sol +++ b/packages/ethereum-contracts/contracts/utils/ERC1820Implementer.sol @@ -1,5 +1,7 @@ -import { IERC1820Implementer } from "@openzeppelin-v5/contracts/interfaces/IERC1820Implementer.sol"; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; +import { IERC1820Implementer } from "@openzeppelin-v5/contracts/interfaces/IERC1820Implementer.sol"; // Copy of OpenZeppelin v4.9.6: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.6/ /**