From d05f78447abcb5d70b4b48a54da1bdaabc216746 Mon Sep 17 00:00:00 2001 From: didi Date: Mon, 16 Dec 2024 15:31:47 +0100 Subject: [PATCH 01/10] alternative API proposal --- .../contracts/apps/CFASuperAppBase.sol | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol b/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol index ec91b3e5e4..1174d51a49 100644 --- a/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol +++ b/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol @@ -103,7 +103,7 @@ abstract contract CFASuperAppBase is ISuperApp { /// @dev override if the SuperApp shall have custom logic invoked when a new flow /// to it is created. - function onFlowCreated( + function onInflowCreated( ISuperToken /*superToken*/, address /*sender*/, bytes calldata ctx @@ -113,7 +113,7 @@ abstract contract CFASuperAppBase is ISuperApp { /// @dev override if the SuperApp shall have custom logic invoked when an existing flow /// to it is updated (flowrate change). - function onFlowUpdated( + function onInflowUpdated( ISuperToken /*superToken*/, address /*sender*/, int96 /*previousFlowRate*/, @@ -127,9 +127,25 @@ abstract contract CFASuperAppBase is ISuperApp { /// to it is deleted (flowrate set to 0). /// Unlike the other callbacks, this method is NOT allowed to revert. /// Failing to satisfy that requirement leads to jailing (defunct SuperApp). - function onFlowDeleted( + function onInflowDeleted( ISuperToken /*superToken*/, address /*sender*/, + int96 /*previousFlowRate*/, + uint256 /*lastUpdated*/, + bytes calldata ctx + ) internal virtual returns (bytes memory /*newCtx*/) { + return ctx; + } + + /// @dev override if the SuperApp shall have custom logic invoked when an outgoing flow + /// is deleted by the receiver (it's not triggered when deleted by the SuperApp itself). + /// A possible implementation is to make outflows "sticky" by simply reopening it. + /// Like onInflowDeleted, this method is NOT allowed to revert. + /// Note: In theory this hook could also be triggered by a liquidation, but this would imply + /// that the SuperApp is insolvent, and would thus be jailed already. + /// Thus in practice this is triggered only when a receiver deletes the flow. + function onOutflowDeleted( + ISuperToken /*superToken*/, address /*receiver*/, int96 /*previousFlowRate*/, uint256 /*lastUpdated*/, @@ -173,7 +189,7 @@ abstract contract CFASuperAppBase is ISuperApp { (address sender, ) = abi.decode(agreementData, (address, address)); return - onFlowCreated( + onInflowCreated( superToken, sender, ctx // userData can be acquired with `host.decodeCtx(ctx).userData` @@ -218,7 +234,7 @@ abstract contract CFASuperAppBase is ISuperApp { (int96 previousFlowRate, uint256 lastUpdated) = abi.decode(cbdata, (int96, uint256)); return - onFlowUpdated( + onInflowUpdated( superToken, sender, previousFlowRate, @@ -272,15 +288,26 @@ abstract contract CFASuperAppBase is ISuperApp { (address sender, address receiver) = abi.decode(agreementData, (address, address)); (uint256 lastUpdated, int96 previousFlowRate) = abi.decode(cbdata, (uint256, int96)); - return - onFlowDeleted( - superToken, - sender, - receiver, - previousFlowRate, - lastUpdated, - ctx + if (receiver == address(this)) { + return + onInflowDeleted( + superToken, + sender, + previousFlowRate, + lastUpdated, + ctx + ); + } else { + return + onOutflowDeleted( + superToken, + receiver, + previousFlowRate, + lastUpdated, + ctx + ) ); + } } From 55190536f02a96e067768b7cf282d58ccb5ce79d Mon Sep 17 00:00:00 2001 From: didi Date: Fri, 20 Dec 2024 18:01:52 +0100 Subject: [PATCH 02/10] fix --- packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol b/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol index 1174d51a49..5ba4cca230 100644 --- a/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol +++ b/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol @@ -305,8 +305,7 @@ abstract contract CFASuperAppBase is ISuperApp { previousFlowRate, lastUpdated, ctx - ) - ); + ); } } From 8cb88a06d0d9cfe20067eea9c33e46b269db06fa Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 2 Jan 2025 17:20:33 +0100 Subject: [PATCH 03/10] complete implementation, tests, doc --- packages/ethereum-contracts/CHANGELOG.md | 7 ++ .../contracts/apps/CFASuperAppBase.sol | 76 ++++++++++++------- .../contracts/apps/SuperAppBase.sol | 5 ++ .../test/foundry/apps/CFASuperAppBase.t.sol | 22 +++++- .../foundry/apps/CFASuperAppBaseTester.t.sol | 13 +++- .../apps/SuperAppTester/FlowSplitter.sol | 21 +++-- 6 files changed, 104 insertions(+), 40 deletions(-) diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index e1e3b10c6f..a7f67e9beb 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -12,6 +12,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed - Fixed deployment of SimpleForwarder (solved an issue which caused batch operation `OPERATION_TYPE_SIMPLE_FORWARD_CALL` to always revert) +# Breaking +- CFASuperAppBase: `onFlowDeleted` from now on only handles events related to incoming flows, while for events triggered by outgoing flows `onOutFlowDeleted` is invoked. + This is safer because the latter case is in many cases unexpected and may thus not be handled correctly, potentially leading to state corruption or SuperApp jailing. + The change is breaking because of a signature change in `onFlowDeleted`. The removal of the now unnecessary `receiver` argument also makes sure + that this change can't without notice break implementations which correctly handled the corner case of an outgoing flow with the previous implementation of CFASuperAppBase. + Many applications may not want/need to handle the case of outgoing flows being deleted, thus don't need to override the newly added `onOutFlowDeleted`. + ## [v1.12.0] ### Added diff --git a/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol b/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol index 5ba4cca230..2e59a1e1ae 100644 --- a/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol +++ b/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol @@ -8,20 +8,27 @@ import { SuperTokenV1Library } from "./SuperTokenV1Library.sol"; * @title abstract base contract for SuperApps using CFA callbacks * @author Superfluid * @dev This contract provides a more convenient API for implementing CFA callbacks. - * It allows to write more concise and readable SuperApps when the full flexibility - * of the low-level agreement callbacks isn't needed. - * The API is tailored for the most common use cases, with the "beforeX" and "afterX" callbacks being + * It allows to write more concise and readable SuperApps. + * The API is tailored for common use cases, with the "beforeX" and "afterX" callbacks being * abstrated into a single "onX" callback for create|update|delete flows. - * For use cases requiring more flexibility (specifically if more data needs to be provided by the before callbacks) - * it's recommended to implement the low-level callbacks directly instead of using this base contract. + * If the previous state provided by this API (`previousFlowRate` and `lastUpdated`) is not sufficient for you use case, + * you should implement the more generic low-level API of `ISuperApp` instead of using this base contract. */ abstract contract CFASuperAppBase is ISuperApp { using SuperTokenV1Library for ISuperToken; + /// ================================================================================= + /// CONSTANTS & IMMUTABLES + /// ================================================================================= + bytes32 public constant CFAV1_TYPE = keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1"); ISuperfluid public immutable HOST; + /// ================================================================================= + /// ERRORS + /// ================================================================================= + /// @dev Thrown when the callback caller is not the host. error UnauthorizedHost(); @@ -31,6 +38,10 @@ abstract contract CFASuperAppBase is ISuperApp { /// @dev Thrown when SuperTokens not accepted by the SuperApp are streamed to it error NotAcceptedSuperToken(); + // ================================================================================= + // SETUP + // ================================================================================= + /** * @dev Creates the contract tied to the provided Superfluid host * @param host_ the Superfluid host the SuperApp belongs to @@ -49,7 +60,7 @@ abstract contract CFASuperAppBase is ISuperApp { * * Note: if the App self-registers on a network with permissioned SuperApp registration, * self-registration can be used only if the tx.origin (EOA) is whitelisted as deployer. - * If a whitelisted factory is used, it needs to call `host.registerApp()` itself. + * If instead a whitelisted factory is used, the factory needs to call `host.registerApp(address app)`. * For more details, see https://github.com/superfluid-finance/protocol-monorepo/wiki/Super-App-White-listing-Guide */ function selfRegister( @@ -72,7 +83,9 @@ abstract contract CFASuperAppBase is ISuperApp { bool activateOnUpdated, bool activateOnDeleted ) public pure returns (uint256 configWord) { + // since only 1 level is allowed by the protocol, we can hardcode APP_LEVEL_FINAL configWord = SuperAppDefinitions.APP_LEVEL_FINAL + // there's no information we want to carry over for create | SuperAppDefinitions.BEFORE_AGREEMENT_CREATED_NOOP; if (!activateOnCreated) { configWord |= SuperAppDefinitions.AFTER_AGREEMENT_CREATED_NOOP; @@ -96,14 +109,13 @@ abstract contract CFASuperAppBase is ISuperApp { return true; } - - // --------------------------------------------------------------------------------------------- - // CFA specific convenience callbacks - // to be overridden and implemented by inheriting SuperApps + // ================================================================================= + // CFA SPECIFIC CALLBACKS - TO BE OVERRIDDEN BY INHERITING SUPERAPPS + // ================================================================================= /// @dev override if the SuperApp shall have custom logic invoked when a new flow /// to it is created. - function onInflowCreated( + function onFlowCreated( ISuperToken /*superToken*/, address /*sender*/, bytes calldata ctx @@ -113,7 +125,7 @@ abstract contract CFASuperAppBase is ISuperApp { /// @dev override if the SuperApp shall have custom logic invoked when an existing flow /// to it is updated (flowrate change). - function onInflowUpdated( + function onFlowUpdated( ISuperToken /*superToken*/, address /*sender*/, int96 /*previousFlowRate*/, @@ -127,7 +139,7 @@ abstract contract CFASuperAppBase is ISuperApp { /// to it is deleted (flowrate set to 0). /// Unlike the other callbacks, this method is NOT allowed to revert. /// Failing to satisfy that requirement leads to jailing (defunct SuperApp). - function onInflowDeleted( + function onFlowDeleted( ISuperToken /*superToken*/, address /*sender*/, int96 /*previousFlowRate*/, @@ -140,10 +152,12 @@ abstract contract CFASuperAppBase is ISuperApp { /// @dev override if the SuperApp shall have custom logic invoked when an outgoing flow /// is deleted by the receiver (it's not triggered when deleted by the SuperApp itself). /// A possible implementation is to make outflows "sticky" by simply reopening it. - /// Like onInflowDeleted, this method is NOT allowed to revert. + /// Like onFlowDeleted, this method is NOT allowed to revert. + /// It's safe to not override this method if the SuperApp doesn't have outgoing flows, + /// or if it doesn't want/need to know if an outgoing flow is deleted by its receiver. /// Note: In theory this hook could also be triggered by a liquidation, but this would imply /// that the SuperApp is insolvent, and would thus be jailed already. - /// Thus in practice this is triggered only when a receiver deletes the flow. + /// Thus in practice this is triggered only when a receiver of an outgoing flow deletes that flow. function onOutflowDeleted( ISuperToken /*superToken*/, address /*receiver*/, @@ -154,12 +168,16 @@ abstract contract CFASuperAppBase is ISuperApp { return ctx; } + // ================================================================================= + // INTERNAL IMPLEMENTATION + // ================================================================================= - // --------------------------------------------------------------------------------------------- - // Low-level callbacks - // Shall NOT be overriden by SuperApps when inheriting from this contract. - // The before-callbacks are implemented to forward data (flowrate, timestamp), - // the after-callbacks invoke the CFA specific specific convenience callbacks. + // The following methods SHALL NOT BE OVERRIDDEN by SuperApps inheriting from this contract. + // If more fine grained control than provided by the onX callbacks is needed, + // you should implement the more generic low-level API of `ISuperApp` instead of using this base contract. + + // The before-callbacks are implemented to relay data (flowrate, timestamp) to the after-callbacks. + // The after-callbacks invoke the more convenient onX callbacks. // CREATED callback @@ -183,13 +201,13 @@ abstract contract CFASuperAppBase is ISuperApp { bytes calldata ctx ) external override returns (bytes memory newCtx) { if (msg.sender != address(HOST)) revert UnauthorizedHost(); - if (!isAcceptedAgreement(agreementClass)) return ctx; + if (!_isAcceptedAgreement(agreementClass)) return ctx; if (!isAcceptedSuperToken(superToken)) revert NotAcceptedSuperToken(); (address sender, ) = abi.decode(agreementData, (address, address)); return - onInflowCreated( + onFlowCreated( superToken, sender, ctx // userData can be acquired with `host.decodeCtx(ctx).userData` @@ -206,7 +224,7 @@ abstract contract CFASuperAppBase is ISuperApp { bytes calldata /*ctx*/ ) external view override returns (bytes memory /*beforeData*/) { if (msg.sender != address(HOST)) revert UnauthorizedHost(); - if (!isAcceptedAgreement(agreementClass)) return "0x"; + if (!_isAcceptedAgreement(agreementClass)) return "0x"; if (!isAcceptedSuperToken(superToken)) revert NotAcceptedSuperToken(); (address sender, ) = abi.decode(agreementData, (address, address)); @@ -227,14 +245,14 @@ abstract contract CFASuperAppBase is ISuperApp { bytes calldata ctx ) external override returns (bytes memory newCtx) { if (msg.sender != address(HOST)) revert UnauthorizedHost(); - if (!isAcceptedAgreement(agreementClass)) return ctx; + if (!_isAcceptedAgreement(agreementClass)) return ctx; if (!isAcceptedSuperToken(superToken)) revert NotAcceptedSuperToken(); (address sender, ) = abi.decode(agreementData, (address, address)); (int96 previousFlowRate, uint256 lastUpdated) = abi.decode(cbdata, (int96, uint256)); return - onInflowUpdated( + onFlowUpdated( superToken, sender, previousFlowRate, @@ -254,7 +272,7 @@ abstract contract CFASuperAppBase is ISuperApp { ) external view override returns (bytes memory /*beforeData*/) { // we're not allowed to revert in this callback, thus just return empty beforeData on failing checks if (msg.sender != address(HOST) - || !isAcceptedAgreement(agreementClass) + || !_isAcceptedAgreement(agreementClass) || !isAcceptedSuperToken(superToken)) { return "0x"; @@ -279,7 +297,7 @@ abstract contract CFASuperAppBase is ISuperApp { ) external override returns (bytes memory newCtx) { // we're not allowed to revert in this callback, thus just return ctx on failing checks if (msg.sender != address(HOST) - || !isAcceptedAgreement(agreementClass) + || !_isAcceptedAgreement(agreementClass) || !isAcceptedSuperToken(superToken)) { return ctx; @@ -290,7 +308,7 @@ abstract contract CFASuperAppBase is ISuperApp { if (receiver == address(this)) { return - onInflowDeleted( + onFlowDeleted( superToken, sender, previousFlowRate, @@ -318,7 +336,7 @@ abstract contract CFASuperAppBase is ISuperApp { * This function can be overridden with custom logic and to revert if desired * Current implementation expects ConstantFlowAgreement */ - function isAcceptedAgreement(address agreementClass) internal view virtual returns (bool) { + function _isAcceptedAgreement(address agreementClass) internal view returns (bool) { return agreementClass == address(HOST.getAgreementClass(CFAV1_TYPE)); } } diff --git a/packages/ethereum-contracts/contracts/apps/SuperAppBase.sol b/packages/ethereum-contracts/contracts/apps/SuperAppBase.sol index cd6c642daa..7c5a78face 100644 --- a/packages/ethereum-contracts/contracts/apps/SuperAppBase.sol +++ b/packages/ethereum-contracts/contracts/apps/SuperAppBase.sol @@ -5,6 +5,11 @@ pragma solidity >= 0.8.11; // solhint-disable-next-line no-global-import import "../interfaces/superfluid/ISuperfluid.sol"; +/** + * @title [DEPRECATED] Base contract which provides a reverting implementation of all ISuperApp methods. + * @author Superfluid + * @custom:deprecated Use an agreement specific base contract (e.g. `CFASuperAppBase`) or implement `ISuperApp`. + */ abstract contract SuperAppBase is ISuperApp { function beforeAgreementCreated( diff --git a/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBase.t.sol b/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBase.t.sol index 13d0cda4f3..5b44f97a66 100644 --- a/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBase.t.sol +++ b/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBase.t.sol @@ -194,7 +194,7 @@ contract CFASuperAppBaseTest is FoundrySuperfluidTester { vm.stopPrank(); } - // test delete flow + // test delete flow to superApp (incoming flow) function testDeleteFlowToSuperApp(int96 flowRate) public { flowRate = int96(bound(flowRate, 1, int96(uint96(type(uint32).max)))); vm.startPrank(alice); @@ -207,13 +207,27 @@ contract CFASuperAppBaseTest is FoundrySuperfluidTester { superToken.deleteFlow(alice, superAppAddress); assertEq(superToken.getFlowRate(alice, superAppAddress), 0, "SuperAppBase: deleteFlow2 | flowRate incorrect"); assertEq(superApp.afterSenderHolder(), alice, "SuperAppBase: deleteFlow2 | afterSenderHolder incorrect"); - assertEq( - superApp.afterReceiverHolder(), superAppAddress, "SuperAppBase: deleteFlow2 | afterReceiverHolder incorrect" - ); assertEq(superApp.oldFlowRateHolder(), flowRate, "SuperAppBase: deleteFlow2 | oldFlowRateHolder incorrect"); vm.stopPrank(); } + // test delete flow from superApp + function testDeleteFlowFromSuperApp(int96 flowRate) public { + flowRate = int96(bound(flowRate, 1, int96(uint96(type(uint32).max)))); + + vm.startPrank(alice); + // fund the superApp and start a stream from it to alice + superToken.transfer(superAppAddress, 1e18); + superApp.startStream(superToken, alice, flowRate); + + // let alice delete the flow, triggering the onOutFlowDeleted callback + superToken.deleteFlow(superAppAddress, alice); + assertEq(superApp.lastUpdateHolder(), block.timestamp, "SuperAppBase: deleteFlow | lastUpdateHolder incorrect"); + assertEq(superApp.oldFlowRateHolder(), flowRate, "SuperAppBase: deleteFlow | oldFlowRateHolder incorrect"); + assertEq(superApp.afterReceiverHolder(), alice, "SuperAppBase: deleteFlow | afterReceiverHolder incorrect"); + vm.stopPrank(); + } + function testMockBeforeAgreementCreated() public { vm.startPrank(alice); bytes memory data = superApp.beforeAgreementCreated( diff --git a/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBaseTester.t.sol b/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBaseTester.t.sol index 48fbb81d27..9acbd9da1e 100644 --- a/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBaseTester.t.sol +++ b/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBaseTester.t.sol @@ -75,7 +75,6 @@ contract CFASuperAppBaseTester is CFASuperAppBase { function onFlowDeleted( ISuperToken, /*superToken*/ address sender, - address receiver, int96 previousFlowRate, uint256 lastUpdated, bytes calldata ctx @@ -83,6 +82,18 @@ contract CFASuperAppBaseTester is CFASuperAppBase { lastUpdateHolder = lastUpdated; oldFlowRateHolder = previousFlowRate; afterSenderHolder = sender; + return ctx; + } + + function onOutflowDeleted( + ISuperToken, /*superToken*/ + address receiver, + int96 previousFlowRate, + uint256 lastUpdated, + bytes calldata ctx + ) internal override returns (bytes memory newCtx) { + lastUpdateHolder = lastUpdated; + oldFlowRateHolder = previousFlowRate; afterReceiverHolder = receiver; return ctx; } diff --git a/packages/ethereum-contracts/test/foundry/apps/SuperAppTester/FlowSplitter.sol b/packages/ethereum-contracts/test/foundry/apps/SuperAppTester/FlowSplitter.sol index 808b9f34bb..16d50f9538 100644 --- a/packages/ethereum-contracts/test/foundry/apps/SuperAppTester/FlowSplitter.sol +++ b/packages/ethereum-contracts/test/foundry/apps/SuperAppTester/FlowSplitter.sol @@ -132,7 +132,6 @@ contract FlowSplitter is CFASuperAppBase { function onFlowDeleted( ISuperToken superToken, address, /*sender*/ - address receiver, int96 previousFlowRate, uint256, /*lastUpdated*/ bytes calldata ctx @@ -145,11 +144,6 @@ contract FlowSplitter is CFASuperAppBase { + acceptedSuperToken.getFlowRate(address(this), sideReceiver) ) - previousFlowRate; - // handle "rogue recipients" with sticky stream - see readme - if (receiver == mainReceiver || receiver == sideReceiver) { - newCtx = superToken.createFlowWithCtx(receiver, previousFlowRate, newCtx); - } - // if there is no more inflow, outflows should be deleted if (remainingInflow <= 0) { newCtx = superToken.deleteFlowWithCtx(address(this), mainReceiver, newCtx); @@ -165,4 +159,19 @@ contract FlowSplitter is CFASuperAppBase { newCtx = superToken.updateFlowWithCtx(sideReceiver, (remainingInflow * sideReceiverPortion) / 1000, newCtx); } } + + function onOutflowDeleted( + ISuperToken superToken, + address receiver, + int96 previousFlowRate, + uint256, /*lastUpdated*/ + bytes calldata ctx + ) internal override returns (bytes memory newCtx) { + newCtx = ctx; + + // handle "rogue recipients" with sticky stream - see readme + if (receiver == mainReceiver || receiver == sideReceiver) { + newCtx = superToken.createFlowWithCtx(receiver, previousFlowRate, newCtx); + } + } } From ee71ae0871b1e3c26d30beaeb14676f70ab01add Mon Sep 17 00:00:00 2001 From: didi Date: Wed, 22 Oct 2025 13:36:48 +0200 Subject: [PATCH 04/10] adjust test --- .../contracts/utils/ERC1820Implementer.sol | 3 +++ .../foundry/apps/SuperTokenV1Library.t.sol | 18 +++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/ethereum-contracts/contracts/utils/ERC1820Implementer.sol b/packages/ethereum-contracts/contracts/utils/ERC1820Implementer.sol index 7138f019bb..4eb43f6574 100644 --- a/packages/ethereum-contracts/contracts/utils/ERC1820Implementer.sol +++ b/packages/ethereum-contracts/contracts/utils/ERC1820Implementer.sol @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + import { IERC1820Implementer } from "@openzeppelin-v5/contracts/interfaces/IERC1820Implementer.sol"; diff --git a/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol b/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol index 8983b1a162..8c6dbdef74 100644 --- a/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol +++ b/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol @@ -556,18 +556,22 @@ contract SuperAppMock is CFASuperAppBase { function onFlowDeleted( ISuperToken superToken, address sender, + int96 previousFlowRate, + uint256 /*lastUpdated*/, + bytes calldata ctx + ) internal virtual override returns (bytes memory /*newCtx*/) { + return _mirrorOrMatchIncomingFlow(superToken, sender, ctx); + } + + // outflow was deleted by the sender we mirror to, we make it "sticky" by simply restoring it. + function onOutflowDeleted( + ISuperToken superToken, address receiver, int96 previousFlowRate, uint256 /*lastUpdated*/, bytes calldata ctx ) internal virtual override returns (bytes memory /*newCtx*/) { - if (receiver == address(this)) { - return _mirrorOrMatchIncomingFlow(superToken, sender, ctx); - } else { - // outflow was deleted by the sender we mirror to, - // we make it "sticky" by simply restoring it. - return superToken.flowWithCtx(receiver, previousFlowRate, ctx); - } + return superToken.flowWithCtx(receiver, previousFlowRate, ctx); } function _mirrorOrMatchIncomingFlow(ISuperToken superToken, address senderAndReceiver, bytes memory ctx) From 00fbe2c0f0c9b1ea57491c70f973a2458d38b06a Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 23 Oct 2025 12:49:15 +0200 Subject: [PATCH 05/10] renamed to onInFlowDeleted and onOutFlowDeleted --- packages/ethereum-contracts/CHANGELOG.md | 9 ++++----- .../contracts/apps/CFASuperAppBase.sol | 12 ++++++------ .../test/foundry/apps/CFASuperAppBaseTester.t.sol | 4 ++-- .../foundry/apps/SuperAppTester/FlowSplitter.sol | 4 ++-- .../test/foundry/apps/SuperTokenV1Library.t.sol | 6 +++--- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index 08468e3a97..554a49f07a 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -72,11 +72,10 @@ subtask(TASK_COMPILE_GET_REMAPPINGS).setAction( - `SuperTokenV1Library.distribute`: return `actualAmount` instead of a bool # Breaking -- CFASuperAppBase: `onFlowDeleted` from now on only handles events related to incoming flows, while for events triggered by outgoing flows `onOutFlowDeleted` is invoked. - This is safer because the latter case is in many cases unexpected and may thus not be handled correctly, potentially leading to state corruption or SuperApp jailing. - The change is breaking because of a signature change in `onFlowDeleted`. The removal of the now unnecessary `receiver` argument also makes sure - that this change can't without notice break implementations which correctly handled the corner case of an outgoing flow with the previous implementation of CFASuperAppBase. - Many applications may not want/need to handle the case of outgoing flows being deleted, thus don't need to override the newly added `onOutFlowDeleted`. +- CFASuperAppBase: `onFlowDeleted` is replaced by `onInFlowDeleted` and `onOutFlowDeleted`. + This is safer because the latter hook handles a case (outgoing flow being deleted by its receiver) which is often not expected. + In the past, apps creating outflows had to explicitly distinguish between the 2 possible triggers in order to avoid potentially invalid state changes or even jailing. + Most apps will want to implement just `onInFlowDeleted`. ## [v1.12.0] diff --git a/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol b/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol index 1e6b873577..3065fa3c6a 100644 --- a/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol +++ b/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol @@ -153,9 +153,9 @@ abstract contract CFASuperAppBase is ISuperApp { /// @dev override if the SuperApp shall have custom logic invoked when an existing flow /// to it is deleted (flowrate set to 0). - /// Unlike the other callbacks, this method is NOT allowed to revert. + /// Unlike the other callbacks, the delete callbacks are NOT allowed to revert. /// Failing to satisfy that requirement leads to jailing (defunct SuperApp). - function onFlowDeleted( + function onInFlowDeleted( ISuperToken /*superToken*/, address /*sender*/, int96 /*previousFlowRate*/, @@ -168,13 +168,13 @@ abstract contract CFASuperAppBase is ISuperApp { /// @dev override if the SuperApp shall have custom logic invoked when an outgoing flow /// is deleted by the receiver (it's not triggered when deleted by the SuperApp itself). /// A possible implementation is to make outflows "sticky" by simply reopening it. - /// Like onFlowDeleted, this method is NOT allowed to revert. + /// Like onInFlowDeleted, this method is NOT allowed to revert. /// It's safe to not override this method if the SuperApp doesn't have outgoing flows, /// or if it doesn't want/need to know if an outgoing flow is deleted by its receiver. /// Note: In theory this hook could also be triggered by a liquidation, but this would imply /// that the SuperApp is insolvent, and would thus be jailed already. /// Thus in practice this is triggered only when a receiver of an outgoing flow deletes that flow. - function onOutflowDeleted( + function onOutFlowDeleted( ISuperToken /*superToken*/, address /*receiver*/, int96 /*previousFlowRate*/, @@ -324,7 +324,7 @@ abstract contract CFASuperAppBase is ISuperApp { if (receiver == address(this)) { return - onFlowDeleted( + onInFlowDeleted( superToken, sender, previousFlowRate, @@ -333,7 +333,7 @@ abstract contract CFASuperAppBase is ISuperApp { ); } else { return - onOutflowDeleted( + onOutFlowDeleted( superToken, receiver, previousFlowRate, diff --git a/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBaseTester.t.sol b/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBaseTester.t.sol index 9acbd9da1e..9026785e29 100644 --- a/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBaseTester.t.sol +++ b/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBaseTester.t.sol @@ -72,7 +72,7 @@ contract CFASuperAppBaseTester is CFASuperAppBase { // DELETE - function onFlowDeleted( + function onInFlowDeleted( ISuperToken, /*superToken*/ address sender, int96 previousFlowRate, @@ -85,7 +85,7 @@ contract CFASuperAppBaseTester is CFASuperAppBase { return ctx; } - function onOutflowDeleted( + function onOutFlowDeleted( ISuperToken, /*superToken*/ address receiver, int96 previousFlowRate, diff --git a/packages/ethereum-contracts/test/foundry/apps/SuperAppTester/FlowSplitter.sol b/packages/ethereum-contracts/test/foundry/apps/SuperAppTester/FlowSplitter.sol index 16d50f9538..76e8f23ce6 100644 --- a/packages/ethereum-contracts/test/foundry/apps/SuperAppTester/FlowSplitter.sol +++ b/packages/ethereum-contracts/test/foundry/apps/SuperAppTester/FlowSplitter.sol @@ -129,7 +129,7 @@ contract FlowSplitter is CFASuperAppBase { ); } - function onFlowDeleted( + function onInFlowDeleted( ISuperToken superToken, address, /*sender*/ int96 previousFlowRate, @@ -160,7 +160,7 @@ contract FlowSplitter is CFASuperAppBase { } } - function onOutflowDeleted( + function onOutFlowDeleted( ISuperToken superToken, address receiver, int96 previousFlowRate, diff --git a/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol b/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol index 8c6dbdef74..eaeaf9688a 100644 --- a/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol +++ b/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol @@ -553,10 +553,10 @@ contract SuperAppMock is CFASuperAppBase { return _mirrorOrMatchIncomingFlow(superToken, sender, ctx); } - function onFlowDeleted( + function onInFlowDeleted( ISuperToken superToken, address sender, - int96 previousFlowRate, + int96 /*previousFlowRate*/, uint256 /*lastUpdated*/, bytes calldata ctx ) internal virtual override returns (bytes memory /*newCtx*/) { @@ -564,7 +564,7 @@ contract SuperAppMock is CFASuperAppBase { } // outflow was deleted by the sender we mirror to, we make it "sticky" by simply restoring it. - function onOutflowDeleted( + function onOutFlowDeleted( ISuperToken superToken, address receiver, int96 previousFlowRate, From 72aa77d06cada5960508061f48a81e55edb06e0d Mon Sep 17 00:00:00 2001 From: didi Date: Thu, 23 Oct 2025 15:40:25 +0200 Subject: [PATCH 06/10] add flowrate argument to onFlowCreated and onFlowUpdated --- .../contracts/apps/CFASuperAppBase.sol | 31 ++++++++++++++----- .../foundry/apps/CFASuperAppBaseTester.t.sol | 3 +- .../foundry/apps/CrossStreamSuperApp.t.sol | 11 +++---- .../apps/SuperAppTester/FlowSplitter.sol | 18 +++++------ .../foundry/apps/SuperTokenV1Library.t.sol | 11 ++++--- 5 files changed, 43 insertions(+), 31 deletions(-) diff --git a/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol b/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol index 3065fa3c6a..b268b55939 100644 --- a/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol +++ b/packages/ethereum-contracts/contracts/apps/CFASuperAppBase.sol @@ -134,6 +134,7 @@ abstract contract CFASuperAppBase is ISuperApp { function onFlowCreated( ISuperToken /*superToken*/, address /*sender*/, + int96 /*flowRate*/, bytes calldata ctx ) internal virtual returns (bytes memory /*newCtx*/) { return ctx; @@ -144,6 +145,7 @@ abstract contract CFASuperAppBase is ISuperApp { function onFlowUpdated( ISuperToken /*superToken*/, address /*sender*/, + int96 /*flowRate*/, int96 /*previousFlowRate*/, uint256 /*lastUpdated*/, bytes calldata ctx @@ -221,11 +223,13 @@ abstract contract CFASuperAppBase is ISuperApp { if (!isAcceptedSuperToken(superToken)) revert NotAcceptedSuperToken(); (address sender, ) = abi.decode(agreementData, (address, address)); + int96 flowRate = superToken.getCFAFlowRate(sender, address(this)); return onFlowCreated( superToken, sender, + flowRate, ctx // userData can be acquired with `host.decodeCtx(ctx).userData` ); } @@ -264,17 +268,28 @@ abstract contract CFASuperAppBase is ISuperApp { if (!_isAcceptedAgreement(agreementClass)) return ctx; if (!isAcceptedSuperToken(superToken)) revert NotAcceptedSuperToken(); + return _afterAgreementUpdatedHelper(superToken, agreementData, cbdata, ctx); + } + + // workaround to stack-too-deep compiler error + function _afterAgreementUpdatedHelper( + ISuperToken superToken, + bytes calldata agreementData, + bytes calldata cbdata, + bytes calldata ctx + ) private returns (bytes memory) { (address sender, ) = abi.decode(agreementData, (address, address)); (int96 previousFlowRate, uint256 lastUpdated) = abi.decode(cbdata, (int96, uint256)); + int96 flowRate = superToken.getCFAFlowRate(sender, address(this)); - return - onFlowUpdated( - superToken, - sender, - previousFlowRate, - lastUpdated, - ctx // userData can be acquired with `host.decodeCtx(ctx).userData` - ); + return onFlowUpdated( + superToken, + sender, + flowRate, + previousFlowRate, + lastUpdated, + ctx // userData can be acquired with `host.decodeCtx(ctx).userData` + ); } // DELETED callbacks diff --git a/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBaseTester.t.sol b/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBaseTester.t.sol index 9026785e29..1f7292a5f2 100644 --- a/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBaseTester.t.sol +++ b/packages/ethereum-contracts/test/foundry/apps/CFASuperAppBaseTester.t.sol @@ -46,7 +46,7 @@ contract CFASuperAppBaseTester is CFASuperAppBase { // CREATE - function onFlowCreated(ISuperToken, /*superToken*/ address sender, bytes calldata ctx) + function onFlowCreated(ISuperToken, /*superToken*/ address sender, int96 /*flowRate*/, bytes calldata ctx) internal override returns (bytes memory) @@ -60,6 +60,7 @@ contract CFASuperAppBaseTester is CFASuperAppBase { function onFlowUpdated( ISuperToken, /*superToken*/ address sender, + int96 /*flowRate*/, int96 previousFlowRate, uint256 lastUpdated, bytes calldata ctx diff --git a/packages/ethereum-contracts/test/foundry/apps/CrossStreamSuperApp.t.sol b/packages/ethereum-contracts/test/foundry/apps/CrossStreamSuperApp.t.sol index 7f7cb660d1..f6949b355d 100644 --- a/packages/ethereum-contracts/test/foundry/apps/CrossStreamSuperApp.t.sol +++ b/packages/ethereum-contracts/test/foundry/apps/CrossStreamSuperApp.t.sol @@ -28,27 +28,24 @@ contract CrossStreamSuperApp is CFASuperAppBase { flowRecipient = z_; } - function onFlowCreated(ISuperToken superToken, address sender, bytes calldata ctx) + function onFlowCreated(ISuperToken superToken, address sender, int96 flowRate, bytes calldata ctx) internal override returns (bytes memory newCtx) { newCtx = ctx; - // get incoming stream - int96 inFlowRate = superToken.getFlowRate(sender, address(this)); - if (prevSender == address(0)) { // first flow to super app creates a flow - newCtx = superToken.createFlowWithCtx(flowRecipient, inFlowRate, newCtx); + newCtx = superToken.createFlowWithCtx(flowRecipient, flowRate, newCtx); } else { // subsequent flows to super app updates and deletes the flow - newCtx = superToken.updateFlowWithCtx(flowRecipient, inFlowRate, newCtx); + newCtx = superToken.updateFlowWithCtx(flowRecipient, flowRate, newCtx); newCtx = superToken.deleteFlowWithCtx(prevSender, address(this), newCtx); } prevSender = sender; - prevFlowRate = inFlowRate; + prevFlowRate = flowRate; } } diff --git a/packages/ethereum-contracts/test/foundry/apps/SuperAppTester/FlowSplitter.sol b/packages/ethereum-contracts/test/foundry/apps/SuperAppTester/FlowSplitter.sol index 76e8f23ce6..7a475b8975 100644 --- a/packages/ethereum-contracts/test/foundry/apps/SuperAppTester/FlowSplitter.sol +++ b/packages/ethereum-contracts/test/foundry/apps/SuperAppTester/FlowSplitter.sol @@ -68,35 +68,32 @@ contract FlowSplitter is CFASuperAppBase { // --------------------------------------------------------------------------------------------- // CALLBACK LOGIC - function onFlowCreated(ISuperToken superToken, address sender, bytes calldata ctx) + function onFlowCreated(ISuperToken superToken, address, /*sender*/ int96 flowRate, bytes calldata ctx) internal override returns (bytes memory newCtx) { newCtx = ctx; - // get inflow rate from sender - int96 inflowRate = superToken.getFlowRate(sender, address(this)); - // if there's no outflow already, create outflows if (superToken.getFlowRate(address(this), mainReceiver) == 0) { newCtx = - superToken.createFlowWithCtx(mainReceiver, (inflowRate * (1000 - sideReceiverPortion)) / 1000, newCtx); + superToken.createFlowWithCtx(mainReceiver, (flowRate * (1000 - sideReceiverPortion)) / 1000, newCtx); - newCtx = superToken.createFlowWithCtx(sideReceiver, (inflowRate * sideReceiverPortion) / 1000, newCtx); + newCtx = superToken.createFlowWithCtx(sideReceiver, (flowRate * sideReceiverPortion) / 1000, newCtx); } // otherwise, there's already outflows which should be increased else { newCtx = superToken.updateFlowWithCtx( mainReceiver, acceptedSuperToken.getFlowRate(address(this), mainReceiver) - + (inflowRate * (1000 - sideReceiverPortion)) / 1000, + + (flowRate * (1000 - sideReceiverPortion)) / 1000, newCtx ); newCtx = superToken.updateFlowWithCtx( sideReceiver, - acceptedSuperToken.getFlowRate(address(this), sideReceiver) + (inflowRate * sideReceiverPortion) / 1000, + acceptedSuperToken.getFlowRate(address(this), sideReceiver) + (flowRate * sideReceiverPortion) / 1000, newCtx ); } @@ -104,7 +101,8 @@ contract FlowSplitter is CFASuperAppBase { function onFlowUpdated( ISuperToken superToken, - address sender, + address, /*sender*/ + int96 flowRate, int96 previousFlowRate, uint256, /*lastUpdated*/ bytes calldata ctx @@ -112,7 +110,7 @@ contract FlowSplitter is CFASuperAppBase { newCtx = ctx; // get inflow rate change from sender - int96 inflowChange = superToken.getFlowRate(sender, address(this)) - previousFlowRate; + int96 inflowChange = flowRate - previousFlowRate; // update outflows newCtx = superToken.updateFlowWithCtx( diff --git a/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol b/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol index eaeaf9688a..552c7eda4f 100644 --- a/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol +++ b/packages/ethereum-contracts/test/foundry/apps/SuperTokenV1Library.t.sol @@ -538,19 +538,21 @@ contract SuperAppMock is CFASuperAppBase { function onFlowCreated( ISuperToken superToken, address sender, + int96 flowRate, bytes calldata ctx ) internal virtual override returns (bytes memory /*newCtx*/) { - return _mirrorOrMatchIncomingFlow(superToken, sender, ctx); + return _mirrorOrMatchIncomingFlow(superToken, sender, flowRate, ctx); } function onFlowUpdated( ISuperToken superToken, address sender, + int96 flowRate, int96 /*previousFlowRate*/, uint256 /*lastUpdated*/, bytes calldata ctx ) internal virtual override returns (bytes memory /*newCtx*/) { - return _mirrorOrMatchIncomingFlow(superToken, sender, ctx); + return _mirrorOrMatchIncomingFlow(superToken, sender, flowRate, ctx); } function onInFlowDeleted( @@ -560,7 +562,7 @@ contract SuperAppMock is CFASuperAppBase { uint256 /*lastUpdated*/, bytes calldata ctx ) internal virtual override returns (bytes memory /*newCtx*/) { - return _mirrorOrMatchIncomingFlow(superToken, sender, ctx); + return _mirrorOrMatchIncomingFlow(superToken, sender, 0, ctx); } // outflow was deleted by the sender we mirror to, we make it "sticky" by simply restoring it. @@ -574,10 +576,9 @@ contract SuperAppMock is CFASuperAppBase { return superToken.flowWithCtx(receiver, previousFlowRate, ctx); } - function _mirrorOrMatchIncomingFlow(ISuperToken superToken, address senderAndReceiver, bytes memory ctx) + function _mirrorOrMatchIncomingFlow(ISuperToken superToken, address senderAndReceiver, int96 flowRate, bytes memory ctx) internal returns (bytes memory newCtx) { - int96 flowRate = superToken.getFlowRate(senderAndReceiver, address(this)); if (aclFlowSender == address(0)) { return superToken.flowWithCtx(senderAndReceiver, flowRate, ctx); } else { From 791591858dcfde5f06a767f92744887fc8cfbb19 Mon Sep 17 00:00:00 2001 From: didi Date: Fri, 24 Oct 2025 12:02:43 +0200 Subject: [PATCH 07/10] fix flaky test --- .../test/foundry/echidna/EchidnaTestCases.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ethereum-contracts/test/foundry/echidna/EchidnaTestCases.t.sol b/packages/ethereum-contracts/test/foundry/echidna/EchidnaTestCases.t.sol index 0199edaca0..2c2e2ce8c9 100644 --- a/packages/ethereum-contracts/test/foundry/echidna/EchidnaTestCases.t.sol +++ b/packages/ethereum-contracts/test/foundry/echidna/EchidnaTestCases.t.sol @@ -26,6 +26,7 @@ contract EchidnaTestCases is FoundrySuperfluidTester { public { vm.assume(member != address(0)); + vm.assume(member != currentPool); vm.assume(flowRate > 0); _helperUpdateMemberUnits(currentPool, alice, member, units); From 3b138f82e138bbd0b45ee4dd22adb831af57e743 Mon Sep 17 00:00:00 2001 From: didi Date: Fri, 24 Oct 2025 12:47:30 +0200 Subject: [PATCH 08/10] ... --- .../test/foundry/echidna/EchidnaTestCases.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ethereum-contracts/test/foundry/echidna/EchidnaTestCases.t.sol b/packages/ethereum-contracts/test/foundry/echidna/EchidnaTestCases.t.sol index 2c2e2ce8c9..fc85819684 100644 --- a/packages/ethereum-contracts/test/foundry/echidna/EchidnaTestCases.t.sol +++ b/packages/ethereum-contracts/test/foundry/echidna/EchidnaTestCases.t.sol @@ -26,7 +26,7 @@ contract EchidnaTestCases is FoundrySuperfluidTester { public { vm.assume(member != address(0)); - vm.assume(member != currentPool); + vm.assume(member != address(currentPool)); vm.assume(flowRate > 0); _helperUpdateMemberUnits(currentPool, alice, member, units); From 37be79f3a93eca20cc56c373bc542653ecd3d641 Mon Sep 17 00:00:00 2001 From: didi Date: Fri, 24 Oct 2025 14:19:03 +0200 Subject: [PATCH 09/10] updated CHANGELOG --- packages/ethereum-contracts/CHANGELOG.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index 554a49f07a..ac37fc2cc2 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -38,6 +38,11 @@ subtask(TASK_COMPILE_GET_REMAPPINGS).setAction( } ); ``` +- CFASuperAppBase: `onFlowDeleted` is replaced by `onInFlowDeleted` and `onOutFlowDeleted`. + This is safer because the latter hook handles a case (outgoing flow being deleted by its receiver) which is often not expected. + In the past, apps creating outflows had to explicitly distinguish between the 2 possible triggers in order to avoid potentially invalid state changes or even jailing. + Most apps will want to implement just `onInFlowDeleted`. +- CFASuperAppBase: added `flowRate` argument to `onFlowCreated` and `onFlowUpdated`. - PoolMemberNFT pruning: `IPoolMemberNFT` and `PoolMemberNFT` removed, `POOL_MEMBER_NFT()` removed from `ISuperToken`. ## [v1.13.0] @@ -71,12 +76,6 @@ subtask(TASK_COMPILE_GET_REMAPPINGS).setAction( - `SuperTokenV1Library.distributeFlow`: return `actualFlowRate` instead of a bool - `SuperTokenV1Library.distribute`: return `actualAmount` instead of a bool -# Breaking -- CFASuperAppBase: `onFlowDeleted` is replaced by `onInFlowDeleted` and `onOutFlowDeleted`. - This is safer because the latter hook handles a case (outgoing flow being deleted by its receiver) which is often not expected. - In the past, apps creating outflows had to explicitly distinguish between the 2 possible triggers in order to avoid potentially invalid state changes or even jailing. - Most apps will want to implement just `onInFlowDeleted`. - ## [v1.12.0] ### Added From ec57d2945c723fb4e9acd51431fabca0b54d3fdd Mon Sep 17 00:00:00 2001 From: didi Date: Fri, 24 Oct 2025 14:21:02 +0200 Subject: [PATCH 10/10] nitpicking --- packages/ethereum-contracts/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index ac37fc2cc2..9bb809ace7 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -38,11 +38,11 @@ subtask(TASK_COMPILE_GET_REMAPPINGS).setAction( } ); ``` -- CFASuperAppBase: `onFlowDeleted` is replaced by `onInFlowDeleted` and `onOutFlowDeleted`. +- `CFASuperAppBase`: `onFlowDeleted` is replaced by `onInFlowDeleted` and `onOutFlowDeleted`. This is safer because the latter hook handles a case (outgoing flow being deleted by its receiver) which is often not expected. In the past, apps creating outflows had to explicitly distinguish between the 2 possible triggers in order to avoid potentially invalid state changes or even jailing. Most apps will want to implement just `onInFlowDeleted`. -- CFASuperAppBase: added `flowRate` argument to `onFlowCreated` and `onFlowUpdated`. +- `CFASuperAppBase`: added `flowRate` argument to `onFlowCreated` and `onFlowUpdated`. - PoolMemberNFT pruning: `IPoolMemberNFT` and `PoolMemberNFT` removed, `POOL_MEMBER_NFT()` removed from `ISuperToken`. ## [v1.13.0]