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/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index 9bb809ace7..9b49b92c4a 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,8 @@ 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. +- 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/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 diff --git a/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol b/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol index ec7a840200..0daabe2234 100644 --- a/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol +++ b/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol @@ -453,6 +453,8 @@ contract ConstantFlowAgreementV1 is ctx, currentContext); } + flowVars.token.emitPseudoTransfer(flowVars.sender, flowVars.receiver); + _requireAvailableBalance(flowVars.token, flowVars.sender, currentContext); } @@ -592,6 +594,8 @@ contract ConstantFlowAgreementV1 is newCtx, currentContext); } } + + 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 ff864bbf30..a9e762d17c 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 tokenEmitPseudoTransfer(ISuperfluidToken superToken, address from, address to) external { + if (superToken.isPool(this, msg.sender) == false) { + revert GDA_ONLY_SUPER_TOKEN_POOL(); + } + + superToken.emitPseudoTransfer(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..1a36d38752 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 != newUnits) { + 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 36e06fa123..d9bcd86e61 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 emitPseudoTransfer(address from, address to) external; } diff --git a/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol b/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol index fa9d4af9ad..5f7b7d0c0f 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,21 @@ 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. + * + * 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 { if (account == address(0)) { @@ -515,7 +529,10 @@ contract SuperToken is } _allowances[account][spender] = amount; - emit Approval(account, spender, amount); + + if (emitEvent) { + emit Approval(account, spender, amount); + } } /** diff --git a/packages/ethereum-contracts/contracts/superfluid/SuperfluidToken.sol b/packages/ethereum-contracts/contracts/superfluid/SuperfluidToken.sol index c75fc48f69..f2c3c6d8f4 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 emitPseudoTransfer(address from, address to) external onlyAgreement { + emit IERC20.Transfer(from, to, 0); + } + /************************************************************************** * Modifiers *************************************************************************/ diff --git a/packages/ethereum-contracts/contracts/utils/ERC1820Implementer.sol b/packages/ethereum-contracts/contracts/utils/ERC1820Implementer.sol index 4eb43f6574..d9137bdf5a 100644 --- a/packages/ethereum-contracts/contracts/utils/ERC1820Implementer.sol +++ b/packages/ethereum-contracts/contracts/utils/ERC1820Implementer.sol @@ -3,7 +3,6 @@ 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/ /** * @dev Implementation of the {IERC1820Implementer} interface. 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/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 () { diff --git a/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol index 2d2183df21..4ce9d93d01 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 testEmitPseudoTransferEvent() 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(); + } } 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",