Skip to content

Commit cdee078

Browse files
feat: flow examples (#37)
* build: install flow as submodules build: update PRBMath build: add flow/lockup foundry profiles feat: implement a FlowUtilities library feat: implement flow creator contract refactor: rename v2 to lockup chore: add scripts in package json for the new profiles * feat: add flow batchable * feat: add flow manager contract * fix: calculation * correct contract name * rename constructor var * fix spelling * fix: deposit functions build: install flow repo via package.json test: add tests for FlowStreamCreator test: add tests for FlowBatchable * chore: polish flow examples * ci: fix build and test commands * build: install flow as NPM package * refactor: rename contract to FlowStreamManager * refactor: declare the flow contract as constant polish code use the latest deployment * refactor: calculate first the duration in ratePerSecondForTimestamps * feat: add all functions in FlowStreamManager --------- Co-authored-by: smol-ninja <[email protected]>
1 parent 689f633 commit cdee078

36 files changed

+515
-20
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ jobs:
5050
- name: "Install the Node.js dependencies"
5151
run: "bun install"
5252

53-
- name: "Build the contracts and print their size"
54-
run: "forge build --sizes"
53+
- name: "Build the contracts"
54+
run: "bun run build"
5555

5656
- name: "Add build summary"
5757
run: |
@@ -78,7 +78,7 @@ jobs:
7878
run: "forge config"
7979

8080
- name: "Run the tests"
81-
run: "forge test"
81+
run: "bun run test"
8282

8383
- name: "Add test summary"
8484
run: |

.solhint.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@
66
"contract-name-camelcase": "off",
77
"func-name-mixedcase": "off",
88
"func-visibility": ["error", { "ignoreConstructors": true }],
9+
"gas-custom-errors": "off",
10+
"immutable-vars-naming": "off",
911
"max-line-length": ["error", 124],
1012
"named-parameters-mapping": "warn",
1113
"no-console": "off",
12-
"not-rely-on-time": "off"
14+
"no-empty-blocks": "off",
15+
"not-rely-on-time": "off",
16+
"one-contract-per-file": "off"
1317
}
1418
}

bun.lockb

779 Bytes
Binary file not shown.

flow/FlowBatchable.sol

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity >=0.8.22;
3+
4+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import { ud21x18, UD21x18 } from "@prb/math/src/UD21x18.sol";
6+
import { ud60x18 } from "@prb/math/src/UD60x18.sol";
7+
import { Broker, ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol";
8+
9+
/// @notice The `Batch` contract, inherited in SablierFlow, allows multiple function calls to be batched together. This
10+
/// enables any possible combination of functions to be executed within a single transaction.
11+
/// @dev For some functions to work, `msg.sender` must have approved this contract to spend USDC.
12+
contract FlowBatchable {
13+
// Sepolia addresses
14+
IERC20 public constant USDC = IERC20(0xf08A50178dfcDe18524640EA6618a1f965821715);
15+
ISablierFlow public constant FLOW = ISablierFlow(0x5Ae8c13f6Ae094887322012425b34b0919097d8A);
16+
17+
/// @dev A function to adjust the rate per second and deposit into a stream in a single transaction.
18+
function adjustRatePerSecondAndDeposit(uint256 streamId) external {
19+
UD21x18 newRatePerSecond = ud21x18(0.0001e18);
20+
uint128 depositAmount = 1000e6;
21+
22+
// Transfer to this contract the amount to deposit in the stream.
23+
USDC.transferFrom(msg.sender, address(this), depositAmount);
24+
25+
// Approve the Sablier contract to spend USDC.
26+
USDC.approve(address(FLOW), depositAmount);
27+
28+
// Fetch the stream recipient.
29+
address recipient = FLOW.getRecipient(streamId);
30+
31+
// The call data declared as bytes.
32+
bytes[] memory calls = new bytes[](2);
33+
calls[0] = abi.encodeCall(FLOW.adjustRatePerSecond, (streamId, newRatePerSecond));
34+
calls[1] = abi.encodeCall(FLOW.deposit, (streamId, depositAmount, msg.sender, recipient));
35+
36+
FLOW.batch(calls);
37+
}
38+
39+
/// @dev A function to create a stream and deposit via a broker in a single transaction.
40+
function createAndDepositViaBroker() external returns (uint256 streamId) {
41+
address sender = msg.sender;
42+
address recipient = address(0xCAFE);
43+
UD21x18 ratePerSecond = ud21x18(0.0001e18);
44+
uint128 depositAmount = 1000e6;
45+
46+
// The broker struct.
47+
Broker memory broker = Broker({
48+
account: address(0xDEAD),
49+
fee: ud60x18(0.0001e18) // the fee percentage
50+
});
51+
52+
// Transfer to this contract the amount to deposit in the stream.
53+
USDC.transferFrom(msg.sender, address(this), depositAmount);
54+
55+
// Approve the Sablier contract to spend USDC.
56+
USDC.approve(address(FLOW), depositAmount);
57+
58+
streamId = FLOW.nextStreamId();
59+
60+
// The call data declared as bytes
61+
bytes[] memory calls = new bytes[](2);
62+
calls[0] = abi.encodeCall(FLOW.create, (sender, recipient, ratePerSecond, USDC, true));
63+
calls[1] = abi.encodeCall(FLOW.depositViaBroker, (streamId, depositAmount, sender, recipient, broker));
64+
65+
// Execute multiple calls in a single transaction using the prepared call data.
66+
FLOW.batch(calls);
67+
}
68+
69+
/// @dev A function to create multiple streams in a single transaction.
70+
function createMultiple() external returns (uint256[] memory streamIds) {
71+
address sender = msg.sender;
72+
address firstRecipient = address(0xCAFE);
73+
address secondRecipient = address(0xBEEF);
74+
UD21x18 firstRatePerSecond = ud21x18(0.0001e18);
75+
UD21x18 secondRatePerSecond = ud21x18(0.0002e18);
76+
77+
// The call data declared as bytes
78+
bytes[] memory calls = new bytes[](2);
79+
calls[0] = abi.encodeCall(FLOW.create, (sender, firstRecipient, firstRatePerSecond, USDC, true));
80+
calls[1] = abi.encodeCall(FLOW.create, (sender, secondRecipient, secondRatePerSecond, USDC, true));
81+
82+
// Prepare the `streamIds` array to return them
83+
uint256 nextStreamId = FLOW.nextStreamId();
84+
streamIds = new uint256[](2);
85+
streamIds[0] = nextStreamId;
86+
streamIds[1] = nextStreamId + 1;
87+
88+
// Execute multiple calls in a single transaction using the prepared call data.
89+
FLOW.batch(calls);
90+
}
91+
92+
/// @dev A function to create multiple streams and deposit via a broker into all the stream in a single transaction.
93+
function createMultipleAndDepositViaBroker() external returns (uint256[] memory streamIds) {
94+
address sender = msg.sender;
95+
address firstRecipient = address(0xCAFE);
96+
address secondRecipient = address(0xBEEF);
97+
UD21x18 ratePerSecond = ud21x18(0.0001e18);
98+
uint128 depositAmount = 1000e6;
99+
100+
// Transfer the deposit amount of USDC tokens to this contract for both streams
101+
USDC.transferFrom(msg.sender, address(this), 2 * depositAmount);
102+
103+
// Approve the Sablier contract to spend USDC.
104+
USDC.approve(address(FLOW), 2 * depositAmount);
105+
106+
// The broker struct
107+
Broker memory broker = Broker({
108+
account: address(0xDEAD),
109+
fee: ud60x18(0.0001e18) // the fee percentage
110+
});
111+
112+
uint256 nextStreamId = FLOW.nextStreamId();
113+
streamIds = new uint256[](2);
114+
streamIds[0] = nextStreamId;
115+
streamIds[1] = nextStreamId + 1;
116+
117+
// We need to have 4 different function calls, 2 for creating streams and 2 for depositing via broker
118+
bytes[] memory calls = new bytes[](4);
119+
calls[0] = abi.encodeCall(FLOW.create, (sender, firstRecipient, ratePerSecond, USDC, true));
120+
calls[1] = abi.encodeCall(FLOW.create, (sender, secondRecipient, ratePerSecond, USDC, true));
121+
calls[2] = abi.encodeCall(FLOW.depositViaBroker, (streamIds[0], depositAmount, sender, firstRecipient, broker));
122+
calls[3] = abi.encodeCall(FLOW.depositViaBroker, (streamIds[1], depositAmount, sender, secondRecipient, broker));
123+
124+
// Execute multiple calls in a single transaction using the prepared call data.
125+
FLOW.batch(calls);
126+
}
127+
128+
/// @dev A function to pause a stream and withdraw the maximum available funds.
129+
function pauseAndWithdrawMax(uint256 streamId) external {
130+
// The call data declared as bytes.
131+
bytes[] memory calls = new bytes[](2);
132+
calls[0] = abi.encodeCall(FLOW.pause, (streamId));
133+
calls[1] = abi.encodeCall(FLOW.withdrawMax, (streamId, address(0xCAFE)));
134+
135+
// Execute multiple calls in a single transaction using the prepared call data.
136+
FLOW.batch(calls);
137+
}
138+
139+
/// @dev A function to void a stream and withdraw what is left.
140+
function voidAndWithdrawMax(uint256 streamId) external {
141+
// The call data declared as bytes
142+
bytes[] memory calls = new bytes[](2);
143+
calls[0] = abi.encodeCall(FLOW.void, (streamId));
144+
calls[1] = abi.encodeCall(FLOW.withdrawMax, (streamId, address(0xCAFE)));
145+
146+
// Execute multiple calls in a single transaction using the prepared call data.
147+
FLOW.batch(calls);
148+
}
149+
150+
/// @dev A function to withdraw maximum available funds from multiple streams in a single transaction.
151+
function withdrawMaxMultiple(uint256[] calldata streamIds) external {
152+
uint256 count = streamIds.length;
153+
154+
// Iterate over the streamIds and prepare the call data for each stream.
155+
bytes[] memory calls = new bytes[](count);
156+
for (uint256 i = 0; i < count; ++i) {
157+
address recipient = FLOW.getRecipient(streamIds[i]);
158+
calls[i] = abi.encodeCall(FLOW.withdrawMax, (streamIds[i], recipient));
159+
}
160+
161+
// Execute multiple calls in a single transaction using the prepared call data.
162+
FLOW.batch(calls);
163+
}
164+
}

flow/FlowBatchable.t.sol

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity >=0.8.22;
3+
4+
import { Test } from "forge-std/src/Test.sol";
5+
6+
import { FlowBatchable } from "./FlowBatchable.sol";
7+
8+
contract FlowBatchable_Test is Test {
9+
FlowBatchable internal batchable;
10+
address internal user;
11+
12+
function setUp() external {
13+
// Fork Ethereum Sepolia
14+
vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_250_564 });
15+
16+
// Deploy the batchable contract
17+
batchable = new FlowBatchable();
18+
19+
user = makeAddr("User");
20+
21+
// Mint some DAI tokens to the test user, which will be pulled by the creator contract
22+
deal({ token: address(batchable.USDC()), to: user, give: 1_000_000e6 });
23+
24+
// Make the test user the `msg.sender` in all following calls
25+
vm.startPrank({ msgSender: user });
26+
27+
// Approve the batchable contract to pull USDC tokens from the test user
28+
batchable.USDC().approve({ spender: address(batchable), value: 1_000_000e6 });
29+
}
30+
31+
function test_CreateMultiple() external {
32+
uint256 nextStreamIdBefore = batchable.FLOW().nextStreamId();
33+
34+
uint256[] memory actualStreamIds = batchable.createMultiple();
35+
uint256[] memory expectedStreamIds = new uint256[](2);
36+
expectedStreamIds[0] = nextStreamIdBefore;
37+
expectedStreamIds[1] = nextStreamIdBefore + 1;
38+
39+
assertEq(actualStreamIds, expectedStreamIds);
40+
}
41+
42+
function test_CreateAndDepositViaBroker() external {
43+
uint256 nextStreamIdBefore = batchable.FLOW().nextStreamId();
44+
45+
uint256[] memory actualStreamIds = batchable.createMultipleAndDepositViaBroker();
46+
uint256[] memory expectedStreamIds = new uint256[](2);
47+
expectedStreamIds[0] = nextStreamIdBefore;
48+
expectedStreamIds[1] = nextStreamIdBefore + 1;
49+
50+
assertEq(actualStreamIds, expectedStreamIds);
51+
}
52+
}

flow/FlowStreamCreator.sol

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity >=0.8.22;
3+
4+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import { UD21x18 } from "@prb/math/src/UD21x18.sol";
6+
import { ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol";
7+
8+
import { FlowUtilities } from "./FlowUtilities.sol";
9+
10+
contract FlowStreamCreator {
11+
// Sepolia addresses
12+
IERC20 public constant USDC = IERC20(0xf08A50178dfcDe18524640EA6618a1f965821715);
13+
ISablierFlow public constant FLOW = ISablierFlow(0x5Ae8c13f6Ae094887322012425b34b0919097d8A);
14+
15+
// Create a stream that sends 1000 USDC per month.
16+
function createStream_1K_PerMonth() external returns (uint256 streamId) {
17+
UD21x18 ratePerSecond =
18+
FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: 1000e6, duration: 30 days });
19+
20+
streamId = FLOW.create({
21+
sender: msg.sender, // The sender will be able to manage the stream
22+
recipient: address(0xCAFE), // The recipient of the streamed assets
23+
ratePerSecond: ratePerSecond, // The rate per second equivalent to 1000 USDC per month
24+
token: USDC, // The token to be streamed
25+
transferable: true // Whether the stream will be transferable or not
26+
});
27+
}
28+
29+
// Create a stream that sends 1,000,000 USDC per year.
30+
function createStream_1M_PerYear() external returns (uint256 streamId) {
31+
UD21x18 ratePerSecond =
32+
FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: 1_000_000e6, duration: 365 days });
33+
34+
streamId = FLOW.create({
35+
sender: msg.sender, // The sender will be able to manage the stream
36+
recipient: address(0xCAFE), // The recipient of the streamed assets
37+
ratePerSecond: ratePerSecond, // The rate per second equivalent to 1,000,00 USDC per year
38+
token: USDC, // The token to be streamed
39+
transferable: true // Whether the stream will be transferable or not
40+
});
41+
}
42+
}

flow/FlowStreamCreator.t.sol

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity >=0.8.22;
3+
4+
import { Test } from "forge-std/src/Test.sol";
5+
6+
import { FlowStreamCreator } from "./FlowStreamCreator.sol";
7+
8+
contract FlowStreamCreator_Test is Test {
9+
FlowStreamCreator internal streamCreator;
10+
address internal user;
11+
12+
function setUp() external {
13+
// Fork Ethereum Sepolia
14+
vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_250_564 });
15+
16+
// Deploy the FlowStreamCreator contract
17+
streamCreator = new FlowStreamCreator();
18+
19+
user = makeAddr("User");
20+
21+
// Mint some DAI tokens to the test user, which will be pulled by the creator contract
22+
deal({ token: address(streamCreator.USDC()), to: user, give: 1_000_000e6 });
23+
24+
// Make the test user the `msg.sender` in all following calls
25+
vm.startPrank({ msgSender: user });
26+
27+
// Approve the streamCreator contract to pull USDC tokens from the test user
28+
streamCreator.USDC().approve({ spender: address(streamCreator), value: 1_000_000e6 });
29+
}
30+
31+
function test_CreateStream_1K_PerMonth() external {
32+
uint256 expectedStreamId = streamCreator.FLOW().nextStreamId();
33+
34+
uint256 actualStreamId = streamCreator.createStream_1K_PerMonth();
35+
assertEq(actualStreamId, expectedStreamId);
36+
37+
// Warp slightly over 30 days so that the debt accumulated is slightly over 1000 USDC.
38+
vm.warp({ newTimestamp: block.timestamp + 30 days + 1 seconds });
39+
40+
assertGe(streamCreator.FLOW().totalDebtOf(actualStreamId), 1000e6);
41+
}
42+
43+
function test_CreateStream_1M_PerYear() external {
44+
uint256 expectedStreamId = streamCreator.FLOW().nextStreamId();
45+
46+
uint256 actualStreamId = streamCreator.createStream_1M_PerYear();
47+
assertEq(actualStreamId, expectedStreamId);
48+
49+
// Warp slightly over 365 days so that the debt accumulated is slightly over 1M USDC.
50+
vm.warp({ newTimestamp: block.timestamp + 365 days + 1 seconds });
51+
52+
assertGe(streamCreator.FLOW().totalDebtOf(actualStreamId), 1_000_000e6);
53+
}
54+
}

0 commit comments

Comments
 (0)