Skip to content

Commit f850be2

Browse files
authored
Merge branch 'dev' into 2025-10-zerotransfers
2 parents 408f1be + 9f2c5fe commit f850be2

File tree

9 files changed

+188
-83
lines changed

9 files changed

+188
-83
lines changed

packages/ethereum-contracts/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ subtask(TASK_COMPILE_GET_REMAPPINGS).setAction(
4040
}
4141
);
4242
```
43+
- `CFASuperAppBase`: `onFlowDeleted` is replaced by `onInFlowDeleted` and `onOutFlowDeleted`.
44+
This is safer because the latter hook handles a case (outgoing flow being deleted by its receiver) which is often not expected.
45+
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.
46+
Most apps will want to implement just `onInFlowDeleted`.
47+
- `CFASuperAppBase`: added `flowRate` argument to `onFlowCreated` and `onFlowUpdated`.
4348
- PoolMemberNFT pruning: `IPoolMemberNFT` and `PoolMemberNFT` removed, `POOL_MEMBER_NFT()` removed from `ISuperToken`.
4449

4550
## [v1.13.0]

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

Lines changed: 99 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,27 @@ import { SuperTokenV1Library } from "./SuperTokenV1Library.sol";
1414
* @title abstract base contract for SuperApps using CFA callbacks
1515
* @author Superfluid
1616
* @dev This contract provides a more convenient API for implementing CFA callbacks.
17-
* It allows to write more concise and readable SuperApps when the full flexibility
18-
* of the low-level agreement callbacks isn't needed.
19-
* The API is tailored for the most common use cases, with the "beforeX" and "afterX" callbacks being
17+
* It allows to write more concise and readable SuperApps.
18+
* The API is tailored for common use cases, with the "beforeX" and "afterX" callbacks being
2019
* abstrated into a single "onX" callback for create|update|delete flows.
21-
* For use cases requiring more flexibility (specifically if more data needs to be provided by the before callbacks)
22-
* it's recommended to implement the low-level callbacks directly instead of using this base contract.
20+
* If the previous state provided by this API (`previousFlowRate` and `lastUpdated`) is not sufficient for you use case,
21+
* you should implement the more generic low-level API of `ISuperApp` instead of using this base contract.
2322
*/
2423
abstract contract CFASuperAppBase is ISuperApp {
2524
using SuperTokenV1Library for ISuperToken;
2625

26+
/// =================================================================================
27+
/// CONSTANTS & IMMUTABLES
28+
/// =================================================================================
29+
2730
bytes32 public constant CFAV1_TYPE = keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1");
2831

2932
ISuperfluid public immutable HOST;
3033

34+
/// =================================================================================
35+
/// ERRORS
36+
/// =================================================================================
37+
3138
/// @dev Thrown when the callback caller is not the host.
3239
error UnauthorizedHost();
3340

@@ -37,6 +44,10 @@ abstract contract CFASuperAppBase is ISuperApp {
3744
/// @dev Thrown when SuperTokens not accepted by the SuperApp are streamed to it
3845
error NotAcceptedSuperToken();
3946

47+
// =================================================================================
48+
// SETUP
49+
// =================================================================================
50+
4051
/**
4152
* @dev Creates the contract tied to the provided Superfluid host
4253
* @param host_ the Superfluid host the SuperApp belongs to
@@ -65,7 +76,7 @@ abstract contract CFASuperAppBase is ISuperApp {
6576
*
6677
* Note: if the App self-registers on a network with permissioned SuperApp registration,
6778
* self-registration can be used only if the tx.origin (EOA) is whitelisted as deployer.
68-
* If a whitelisted factory is used, it needs to call `host.registerApp()` itself.
79+
* If instead a whitelisted factory is used, the factory needs to call `host.registerApp(address app)`.
6980
* For more details, see https://github.com/superfluid-finance/protocol-monorepo/wiki/Super-App-White-listing-Guide
7081
*/
7182
function selfRegister(
@@ -88,7 +99,9 @@ abstract contract CFASuperAppBase is ISuperApp {
8899
bool activateOnUpdated,
89100
bool activateOnDeleted
90101
) public pure returns (uint256 configWord) {
102+
// since only 1 level is allowed by the protocol, we can hardcode APP_LEVEL_FINAL
91103
configWord = SuperAppDefinitions.APP_LEVEL_FINAL
104+
// there's no information we want to carry over for create
92105
| SuperAppDefinitions.BEFORE_AGREEMENT_CREATED_NOOP;
93106
if (!activateOnCreated) {
94107
configWord |= SuperAppDefinitions.AFTER_AGREEMENT_CREATED_NOOP;
@@ -112,16 +125,16 @@ abstract contract CFASuperAppBase is ISuperApp {
112125
return true;
113126
}
114127

115-
116-
// ---------------------------------------------------------------------------------------------
117-
// CFA specific convenience callbacks
118-
// to be overridden and implemented by inheriting SuperApps
128+
// =================================================================================
129+
// CFA SPECIFIC CALLBACKS - TO BE OVERRIDDEN BY INHERITING SUPERAPPS
130+
// =================================================================================
119131

120132
/// @dev override if the SuperApp shall have custom logic invoked when a new flow
121133
/// to it is created.
122134
function onFlowCreated(
123135
ISuperToken /*superToken*/,
124136
address /*sender*/,
137+
int96 /*flowRate*/,
125138
bytes calldata ctx
126139
) internal virtual returns (bytes memory /*newCtx*/) {
127140
return ctx;
@@ -132,6 +145,7 @@ abstract contract CFASuperAppBase is ISuperApp {
132145
function onFlowUpdated(
133146
ISuperToken /*superToken*/,
134147
address /*sender*/,
148+
int96 /*flowRate*/,
135149
int96 /*previousFlowRate*/,
136150
uint256 /*lastUpdated*/,
137151
bytes calldata ctx
@@ -141,11 +155,29 @@ abstract contract CFASuperAppBase is ISuperApp {
141155

142156
/// @dev override if the SuperApp shall have custom logic invoked when an existing flow
143157
/// to it is deleted (flowrate set to 0).
144-
/// Unlike the other callbacks, this method is NOT allowed to revert.
158+
/// Unlike the other callbacks, the delete callbacks are NOT allowed to revert.
145159
/// Failing to satisfy that requirement leads to jailing (defunct SuperApp).
146-
function onFlowDeleted(
160+
function onInFlowDeleted(
147161
ISuperToken /*superToken*/,
148162
address /*sender*/,
163+
int96 /*previousFlowRate*/,
164+
uint256 /*lastUpdated*/,
165+
bytes calldata ctx
166+
) internal virtual returns (bytes memory /*newCtx*/) {
167+
return ctx;
168+
}
169+
170+
/// @dev override if the SuperApp shall have custom logic invoked when an outgoing flow
171+
/// is deleted by the receiver (it's not triggered when deleted by the SuperApp itself).
172+
/// A possible implementation is to make outflows "sticky" by simply reopening it.
173+
/// Like onInFlowDeleted, this method is NOT allowed to revert.
174+
/// It's safe to not override this method if the SuperApp doesn't have outgoing flows,
175+
/// or if it doesn't want/need to know if an outgoing flow is deleted by its receiver.
176+
/// Note: In theory this hook could also be triggered by a liquidation, but this would imply
177+
/// that the SuperApp is insolvent, and would thus be jailed already.
178+
/// Thus in practice this is triggered only when a receiver of an outgoing flow deletes that flow.
179+
function onOutFlowDeleted(
180+
ISuperToken /*superToken*/,
149181
address /*receiver*/,
150182
int96 /*previousFlowRate*/,
151183
uint256 /*lastUpdated*/,
@@ -154,12 +186,16 @@ abstract contract CFASuperAppBase is ISuperApp {
154186
return ctx;
155187
}
156188

189+
// =================================================================================
190+
// INTERNAL IMPLEMENTATION
191+
// =================================================================================
157192

158-
// ---------------------------------------------------------------------------------------------
159-
// Low-level callbacks
160-
// Shall NOT be overriden by SuperApps when inheriting from this contract.
161-
// The before-callbacks are implemented to forward data (flowrate, timestamp),
162-
// the after-callbacks invoke the CFA specific specific convenience callbacks.
193+
// The following methods SHALL NOT BE OVERRIDDEN by SuperApps inheriting from this contract.
194+
// If more fine grained control than provided by the onX callbacks is needed,
195+
// you should implement the more generic low-level API of `ISuperApp` instead of using this base contract.
196+
197+
// The before-callbacks are implemented to relay data (flowrate, timestamp) to the after-callbacks.
198+
// The after-callbacks invoke the more convenient onX callbacks.
163199

164200
// CREATED callback
165201

@@ -183,15 +219,17 @@ abstract contract CFASuperAppBase is ISuperApp {
183219
bytes calldata ctx
184220
) external override returns (bytes memory newCtx) {
185221
if (msg.sender != address(HOST)) revert UnauthorizedHost();
186-
if (!isAcceptedAgreement(agreementClass)) return ctx;
222+
if (!_isAcceptedAgreement(agreementClass)) return ctx;
187223
if (!isAcceptedSuperToken(superToken)) revert NotAcceptedSuperToken();
188224

189225
(address sender, ) = abi.decode(agreementData, (address, address));
226+
int96 flowRate = superToken.getCFAFlowRate(sender, address(this));
190227

191228
return
192229
onFlowCreated(
193230
superToken,
194231
sender,
232+
flowRate,
195233
ctx // userData can be acquired with `host.decodeCtx(ctx).userData`
196234
);
197235
}
@@ -206,7 +244,7 @@ abstract contract CFASuperAppBase is ISuperApp {
206244
bytes calldata /*ctx*/
207245
) external view override returns (bytes memory /*beforeData*/) {
208246
if (msg.sender != address(HOST)) revert UnauthorizedHost();
209-
if (!isAcceptedAgreement(agreementClass)) return "0x";
247+
if (!_isAcceptedAgreement(agreementClass)) return "0x";
210248
if (!isAcceptedSuperToken(superToken)) revert NotAcceptedSuperToken();
211249

212250
(address sender, ) = abi.decode(agreementData, (address, address));
@@ -227,20 +265,31 @@ abstract contract CFASuperAppBase is ISuperApp {
227265
bytes calldata ctx
228266
) external override returns (bytes memory newCtx) {
229267
if (msg.sender != address(HOST)) revert UnauthorizedHost();
230-
if (!isAcceptedAgreement(agreementClass)) return ctx;
268+
if (!_isAcceptedAgreement(agreementClass)) return ctx;
231269
if (!isAcceptedSuperToken(superToken)) revert NotAcceptedSuperToken();
232270

271+
return _afterAgreementUpdatedHelper(superToken, agreementData, cbdata, ctx);
272+
}
273+
274+
// workaround to stack-too-deep compiler error
275+
function _afterAgreementUpdatedHelper(
276+
ISuperToken superToken,
277+
bytes calldata agreementData,
278+
bytes calldata cbdata,
279+
bytes calldata ctx
280+
) private returns (bytes memory) {
233281
(address sender, ) = abi.decode(agreementData, (address, address));
234282
(int96 previousFlowRate, uint256 lastUpdated) = abi.decode(cbdata, (int96, uint256));
283+
int96 flowRate = superToken.getCFAFlowRate(sender, address(this));
235284

236-
return
237-
onFlowUpdated(
238-
superToken,
239-
sender,
240-
previousFlowRate,
241-
lastUpdated,
242-
ctx // userData can be acquired with `host.decodeCtx(ctx).userData`
243-
);
285+
return onFlowUpdated(
286+
superToken,
287+
sender,
288+
flowRate,
289+
previousFlowRate,
290+
lastUpdated,
291+
ctx // userData can be acquired with `host.decodeCtx(ctx).userData`
292+
);
244293
}
245294

246295
// DELETED callbacks
@@ -254,7 +303,7 @@ abstract contract CFASuperAppBase is ISuperApp {
254303
) external view override returns (bytes memory /*beforeData*/) {
255304
// we're not allowed to revert in this callback, thus just return empty beforeData on failing checks
256305
if (msg.sender != address(HOST)
257-
|| !isAcceptedAgreement(agreementClass)
306+
|| !_isAcceptedAgreement(agreementClass)
258307
|| !isAcceptedSuperToken(superToken))
259308
{
260309
return "0x";
@@ -279,7 +328,7 @@ abstract contract CFASuperAppBase is ISuperApp {
279328
) external override returns (bytes memory newCtx) {
280329
// we're not allowed to revert in this callback, thus just return ctx on failing checks
281330
if (msg.sender != address(HOST)
282-
|| !isAcceptedAgreement(agreementClass)
331+
|| !_isAcceptedAgreement(agreementClass)
283332
|| !isAcceptedSuperToken(superToken))
284333
{
285334
return ctx;
@@ -288,15 +337,25 @@ abstract contract CFASuperAppBase is ISuperApp {
288337
(address sender, address receiver) = abi.decode(agreementData, (address, address));
289338
(uint256 lastUpdated, int96 previousFlowRate) = abi.decode(cbdata, (uint256, int96));
290339

291-
return
292-
onFlowDeleted(
293-
superToken,
294-
sender,
295-
receiver,
296-
previousFlowRate,
297-
lastUpdated,
298-
ctx
299-
);
340+
if (receiver == address(this)) {
341+
return
342+
onInFlowDeleted(
343+
superToken,
344+
sender,
345+
previousFlowRate,
346+
lastUpdated,
347+
ctx
348+
);
349+
} else {
350+
return
351+
onOutFlowDeleted(
352+
superToken,
353+
receiver,
354+
previousFlowRate,
355+
lastUpdated,
356+
ctx
357+
);
358+
}
300359
}
301360

302361

@@ -308,7 +367,7 @@ abstract contract CFASuperAppBase is ISuperApp {
308367
* This function can be overridden with custom logic and to revert if desired
309368
* Current implementation expects ConstantFlowAgreement
310369
*/
311-
function isAcceptedAgreement(address agreementClass) internal view virtual returns (bool) {
370+
function _isAcceptedAgreement(address agreementClass) internal view returns (bool) {
312371
return agreementClass == address(HOST.getAgreementClass(CFAV1_TYPE));
313372
}
314373
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ pragma solidity >= 0.8.11;
55
// solhint-disable-next-line no-global-import
66
import "../interfaces/superfluid/ISuperfluid.sol";
77

8+
/**
9+
* @title [DEPRECATED] Base contract which provides a reverting implementation of all ISuperApp methods.
10+
* @author Superfluid
11+
* @custom:deprecated Use an agreement specific base contract (e.g. `CFASuperAppBase`) or implement `ISuperApp`.
12+
*/
813
abstract contract SuperAppBase is ISuperApp {
914

1015
function beforeAgreementCreated(

packages/ethereum-contracts/test/foundry/apps/CFASuperAppBase.t.sol

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ contract CFASuperAppBaseTest is FoundrySuperfluidTester {
194194
vm.stopPrank();
195195
}
196196

197-
// test delete flow
197+
// test delete flow to superApp (incoming flow)
198198
function testDeleteFlowToSuperApp(int96 flowRate) public {
199199
flowRate = int96(bound(flowRate, 1, int96(uint96(type(uint32).max))));
200200
vm.startPrank(alice);
@@ -207,13 +207,27 @@ contract CFASuperAppBaseTest is FoundrySuperfluidTester {
207207
superToken.deleteFlow(alice, superAppAddress);
208208
assertEq(superToken.getFlowRate(alice, superAppAddress), 0, "SuperAppBase: deleteFlow2 | flowRate incorrect");
209209
assertEq(superApp.afterSenderHolder(), alice, "SuperAppBase: deleteFlow2 | afterSenderHolder incorrect");
210-
assertEq(
211-
superApp.afterReceiverHolder(), superAppAddress, "SuperAppBase: deleteFlow2 | afterReceiverHolder incorrect"
212-
);
213210
assertEq(superApp.oldFlowRateHolder(), flowRate, "SuperAppBase: deleteFlow2 | oldFlowRateHolder incorrect");
214211
vm.stopPrank();
215212
}
216213

214+
// test delete flow from superApp
215+
function testDeleteFlowFromSuperApp(int96 flowRate) public {
216+
flowRate = int96(bound(flowRate, 1, int96(uint96(type(uint32).max))));
217+
218+
vm.startPrank(alice);
219+
// fund the superApp and start a stream from it to alice
220+
superToken.transfer(superAppAddress, 1e18);
221+
superApp.startStream(superToken, alice, flowRate);
222+
223+
// let alice delete the flow, triggering the onOutFlowDeleted callback
224+
superToken.deleteFlow(superAppAddress, alice);
225+
assertEq(superApp.lastUpdateHolder(), block.timestamp, "SuperAppBase: deleteFlow | lastUpdateHolder incorrect");
226+
assertEq(superApp.oldFlowRateHolder(), flowRate, "SuperAppBase: deleteFlow | oldFlowRateHolder incorrect");
227+
assertEq(superApp.afterReceiverHolder(), alice, "SuperAppBase: deleteFlow | afterReceiverHolder incorrect");
228+
vm.stopPrank();
229+
}
230+
217231
function testMockBeforeAgreementCreated() public {
218232
vm.startPrank(alice);
219233
bytes memory data = superApp.beforeAgreementCreated(

packages/ethereum-contracts/test/foundry/apps/CFASuperAppBaseTester.t.sol

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ contract CFASuperAppBaseTester is CFASuperAppBase {
4646

4747
// CREATE
4848

49-
function onFlowCreated(ISuperToken, /*superToken*/ address sender, bytes calldata ctx)
49+
function onFlowCreated(ISuperToken, /*superToken*/ address sender, int96 /*flowRate*/, bytes calldata ctx)
5050
internal
5151
override
5252
returns (bytes memory)
@@ -60,6 +60,7 @@ contract CFASuperAppBaseTester is CFASuperAppBase {
6060
function onFlowUpdated(
6161
ISuperToken, /*superToken*/
6262
address sender,
63+
int96 /*flowRate*/,
6364
int96 previousFlowRate,
6465
uint256 lastUpdated,
6566
bytes calldata ctx
@@ -72,17 +73,28 @@ contract CFASuperAppBaseTester is CFASuperAppBase {
7273

7374
// DELETE
7475

75-
function onFlowDeleted(
76+
function onInFlowDeleted(
7677
ISuperToken, /*superToken*/
7778
address sender,
78-
address receiver,
7979
int96 previousFlowRate,
8080
uint256 lastUpdated,
8181
bytes calldata ctx
8282
) internal override returns (bytes memory newCtx) {
8383
lastUpdateHolder = lastUpdated;
8484
oldFlowRateHolder = previousFlowRate;
8585
afterSenderHolder = sender;
86+
return ctx;
87+
}
88+
89+
function onOutFlowDeleted(
90+
ISuperToken, /*superToken*/
91+
address receiver,
92+
int96 previousFlowRate,
93+
uint256 lastUpdated,
94+
bytes calldata ctx
95+
) internal override returns (bytes memory newCtx) {
96+
lastUpdateHolder = lastUpdated;
97+
oldFlowRateHolder = previousFlowRate;
8698
afterReceiverHolder = receiver;
8799
return ctx;
88100
}

0 commit comments

Comments
 (0)