Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: flow examples #37

Merged
merged 15 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ jobs:
- name: "Install the Node.js dependencies"
run: "bun install"

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

- name: "Add build summary"
run: |
Expand All @@ -78,7 +78,7 @@ jobs:
run: "forge config"

- name: "Run the tests"
run: "forge test"
run: "bun run test"

- name: "Add test summary"
run: |
Expand Down
6 changes: 5 additions & 1 deletion .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
"contract-name-camelcase": "off",
"func-name-mixedcase": "off",
"func-visibility": ["error", { "ignoreConstructors": true }],
"gas-custom-errors": "off",
"immutable-vars-naming": "off",
"max-line-length": ["error", 124],
"named-parameters-mapping": "warn",
"no-console": "off",
"not-rely-on-time": "off"
"no-empty-blocks": "off",
"not-rely-on-time": "off",
"one-contract-per-file": "off"
}
}
Binary file modified bun.lockb
Binary file not shown.
164 changes: 164 additions & 0 deletions flow/FlowBatchable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ud21x18, UD21x18 } from "@prb/math/src/UD21x18.sol";
import { ud60x18 } from "@prb/math/src/UD60x18.sol";
import { Broker, ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol";

/// @notice The `Batch` contract, inherited in SablierFlow, allows multiple function calls to be batched together. This
/// enables any possible combination of functions to be executed within a single transaction.
/// @dev For some functions to work, `msg.sender` must have approved this contract to spend USDC.
contract FlowBatchable {
// Sepolia addresses
IERC20 public constant USDC = IERC20(0xf08A50178dfcDe18524640EA6618a1f965821715);
ISablierFlow public constant FLOW = ISablierFlow(0x5Ae8c13f6Ae094887322012425b34b0919097d8A);

/// @dev A function to adjust the rate per second and deposit into a stream in a single transaction.
function adjustRatePerSecondAndDeposit(uint256 streamId) external {
UD21x18 newRatePerSecond = ud21x18(0.0001e18);
uint128 depositAmount = 1000e6;

// Transfer to this contract the amount to deposit in the stream.
USDC.transferFrom(msg.sender, address(this), depositAmount);

// Approve the Sablier contract to spend USDC.
USDC.approve(address(FLOW), depositAmount);

// Fetch the stream recipient.
address recipient = FLOW.getRecipient(streamId);

// The call data declared as bytes.
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeCall(FLOW.adjustRatePerSecond, (streamId, newRatePerSecond));
calls[1] = abi.encodeCall(FLOW.deposit, (streamId, depositAmount, msg.sender, recipient));

FLOW.batch(calls);
}

/// @dev A function to create a stream and deposit via a broker in a single transaction.
function createAndDepositViaBroker() external returns (uint256 streamId) {
address sender = msg.sender;
address recipient = address(0xCAFE);
UD21x18 ratePerSecond = ud21x18(0.0001e18);
uint128 depositAmount = 1000e6;

// The broker struct.
Broker memory broker = Broker({
account: address(0xDEAD),
fee: ud60x18(0.0001e18) // the fee percentage
});

// Transfer to this contract the amount to deposit in the stream.
USDC.transferFrom(msg.sender, address(this), depositAmount);

// Approve the Sablier contract to spend USDC.
USDC.approve(address(FLOW), depositAmount);

streamId = FLOW.nextStreamId();

// The call data declared as bytes
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeCall(FLOW.create, (sender, recipient, ratePerSecond, USDC, true));
calls[1] = abi.encodeCall(FLOW.depositViaBroker, (streamId, depositAmount, sender, recipient, broker));

// Execute multiple calls in a single transaction using the prepared call data.
FLOW.batch(calls);
}

/// @dev A function to create multiple streams in a single transaction.
function createMultiple() external returns (uint256[] memory streamIds) {
address sender = msg.sender;
address firstRecipient = address(0xCAFE);
address secondRecipient = address(0xBEEF);
UD21x18 firstRatePerSecond = ud21x18(0.0001e18);
UD21x18 secondRatePerSecond = ud21x18(0.0002e18);

// The call data declared as bytes
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeCall(FLOW.create, (sender, firstRecipient, firstRatePerSecond, USDC, true));
calls[1] = abi.encodeCall(FLOW.create, (sender, secondRecipient, secondRatePerSecond, USDC, true));

// Prepare the `streamIds` array to return them
uint256 nextStreamId = FLOW.nextStreamId();
streamIds = new uint256[](2);
streamIds[0] = nextStreamId;
streamIds[1] = nextStreamId + 1;

// Execute multiple calls in a single transaction using the prepared call data.
FLOW.batch(calls);
}

/// @dev A function to create multiple streams and deposit via a broker into all the stream in a single transaction.
function createMultipleAndDepositViaBroker() external returns (uint256[] memory streamIds) {
address sender = msg.sender;
address firstRecipient = address(0xCAFE);
address secondRecipient = address(0xBEEF);
UD21x18 ratePerSecond = ud21x18(0.0001e18);
uint128 depositAmount = 1000e6;

// Transfer the deposit amount of USDC tokens to this contract for both streams
USDC.transferFrom(msg.sender, address(this), 2 * depositAmount);

// Approve the Sablier contract to spend USDC.
USDC.approve(address(FLOW), 2 * depositAmount);

// The broker struct
Broker memory broker = Broker({
account: address(0xDEAD),
fee: ud60x18(0.0001e18) // the fee percentage
});

uint256 nextStreamId = FLOW.nextStreamId();
streamIds = new uint256[](2);
streamIds[0] = nextStreamId;
streamIds[1] = nextStreamId + 1;

// We need to have 4 different function calls, 2 for creating streams and 2 for depositing via broker
bytes[] memory calls = new bytes[](4);
calls[0] = abi.encodeCall(FLOW.create, (sender, firstRecipient, ratePerSecond, USDC, true));
calls[1] = abi.encodeCall(FLOW.create, (sender, secondRecipient, ratePerSecond, USDC, true));
calls[2] = abi.encodeCall(FLOW.depositViaBroker, (streamIds[0], depositAmount, sender, firstRecipient, broker));
calls[3] = abi.encodeCall(FLOW.depositViaBroker, (streamIds[1], depositAmount, sender, secondRecipient, broker));

// Execute multiple calls in a single transaction using the prepared call data.
FLOW.batch(calls);
}

/// @dev A function to pause a stream and withdraw the maximum available funds.
function pauseAndWithdrawMax(uint256 streamId) external {
// The call data declared as bytes.
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeCall(FLOW.pause, (streamId));
calls[1] = abi.encodeCall(FLOW.withdrawMax, (streamId, address(0xCAFE)));

// Execute multiple calls in a single transaction using the prepared call data.
FLOW.batch(calls);
}

/// @dev A function to void a stream and withdraw what is left.
function voidAndWithdrawMax(uint256 streamId) external {
// The call data declared as bytes
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeCall(FLOW.void, (streamId));
calls[1] = abi.encodeCall(FLOW.withdrawMax, (streamId, address(0xCAFE)));

// Execute multiple calls in a single transaction using the prepared call data.
FLOW.batch(calls);
}

/// @dev A function to withdraw maximum available funds from multiple streams in a single transaction.
function withdrawMaxMultiple(uint256[] calldata streamIds) external {
uint256 count = streamIds.length;

// Iterate over the streamIds and prepare the call data for each stream.
bytes[] memory calls = new bytes[](count);
for (uint256 i = 0; i < count; ++i) {
address recipient = FLOW.getRecipient(streamIds[i]);
calls[i] = abi.encodeCall(FLOW.withdrawMax, (streamIds[i], recipient));
}

// Execute multiple calls in a single transaction using the prepared call data.
FLOW.batch(calls);
}
}
52 changes: 52 additions & 0 deletions flow/FlowBatchable.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;

import { Test } from "forge-std/src/Test.sol";

import { FlowBatchable } from "./FlowBatchable.sol";

contract FlowBatchable_Test is Test {
FlowBatchable internal batchable;
address internal user;

function setUp() external {
// Fork Ethereum Sepolia
vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_250_564 });

// Deploy the batchable contract
batchable = new FlowBatchable();

user = makeAddr("User");

// Mint some DAI tokens to the test user, which will be pulled by the creator contract
deal({ token: address(batchable.USDC()), to: user, give: 1_000_000e6 });

// Make the test user the `msg.sender` in all following calls
vm.startPrank({ msgSender: user });

// Approve the batchable contract to pull USDC tokens from the test user
batchable.USDC().approve({ spender: address(batchable), value: 1_000_000e6 });
}

function test_CreateMultiple() external {
uint256 nextStreamIdBefore = batchable.FLOW().nextStreamId();

uint256[] memory actualStreamIds = batchable.createMultiple();
uint256[] memory expectedStreamIds = new uint256[](2);
expectedStreamIds[0] = nextStreamIdBefore;
expectedStreamIds[1] = nextStreamIdBefore + 1;

assertEq(actualStreamIds, expectedStreamIds);
}

function test_CreateAndDepositViaBroker() external {
uint256 nextStreamIdBefore = batchable.FLOW().nextStreamId();

uint256[] memory actualStreamIds = batchable.createMultipleAndDepositViaBroker();
uint256[] memory expectedStreamIds = new uint256[](2);
expectedStreamIds[0] = nextStreamIdBefore;
expectedStreamIds[1] = nextStreamIdBefore + 1;

assertEq(actualStreamIds, expectedStreamIds);
}
}
42 changes: 42 additions & 0 deletions flow/FlowStreamCreator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { UD21x18 } from "@prb/math/src/UD21x18.sol";
import { ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol";

import { FlowUtilities } from "./FlowUtilities.sol";

contract FlowStreamCreator {
// Sepolia addresses
IERC20 public constant USDC = IERC20(0xf08A50178dfcDe18524640EA6618a1f965821715);
ISablierFlow public constant FLOW = ISablierFlow(0x5Ae8c13f6Ae094887322012425b34b0919097d8A);

// Create a stream that sends 1000 USDC per month.
function createStream_1K_PerMonth() external returns (uint256 streamId) {
UD21x18 ratePerSecond =
FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: 1000e6, duration: 30 days });

streamId = FLOW.create({
sender: msg.sender, // The sender will be able to manage the stream
recipient: address(0xCAFE), // The recipient of the streamed assets
ratePerSecond: ratePerSecond, // The rate per second equivalent to 1000 USDC per month
token: USDC, // The token to be streamed
transferable: true // Whether the stream will be transferable or not
});
}

// Create a stream that sends 1,000,000 USDC per year.
function createStream_1M_PerYear() external returns (uint256 streamId) {
UD21x18 ratePerSecond =
FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: 1_000_000e6, duration: 365 days });

streamId = FLOW.create({
sender: msg.sender, // The sender will be able to manage the stream
recipient: address(0xCAFE), // The recipient of the streamed assets
ratePerSecond: ratePerSecond, // The rate per second equivalent to 1,000,00 USDC per year
token: USDC, // The token to be streamed
transferable: true // Whether the stream will be transferable or not
});
}
}
54 changes: 54 additions & 0 deletions flow/FlowStreamCreator.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;

import { Test } from "forge-std/src/Test.sol";

import { FlowStreamCreator } from "./FlowStreamCreator.sol";

contract FlowStreamCreator_Test is Test {
FlowStreamCreator internal streamCreator;
address internal user;

function setUp() external {
// Fork Ethereum Sepolia
vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_250_564 });

// Deploy the FlowStreamCreator contract
streamCreator = new FlowStreamCreator();

user = makeAddr("User");

// Mint some DAI tokens to the test user, which will be pulled by the creator contract
deal({ token: address(streamCreator.USDC()), to: user, give: 1_000_000e6 });

// Make the test user the `msg.sender` in all following calls
vm.startPrank({ msgSender: user });

// Approve the streamCreator contract to pull USDC tokens from the test user
streamCreator.USDC().approve({ spender: address(streamCreator), value: 1_000_000e6 });
}

function test_CreateStream_1K_PerMonth() external {
uint256 expectedStreamId = streamCreator.FLOW().nextStreamId();

uint256 actualStreamId = streamCreator.createStream_1K_PerMonth();
assertEq(actualStreamId, expectedStreamId);

// Warp slightly over 30 days so that the debt accumulated is slightly over 1000 USDC.
vm.warp({ newTimestamp: block.timestamp + 30 days + 1 seconds });

assertGe(streamCreator.FLOW().totalDebtOf(actualStreamId), 1000e6);
}

function test_CreateStream_1M_PerYear() external {
uint256 expectedStreamId = streamCreator.FLOW().nextStreamId();

uint256 actualStreamId = streamCreator.createStream_1M_PerYear();
assertEq(actualStreamId, expectedStreamId);

// Warp slightly over 365 days so that the debt accumulated is slightly over 1M USDC.
vm.warp({ newTimestamp: block.timestamp + 365 days + 1 seconds });

assertGe(streamCreator.FLOW().totalDebtOf(actualStreamId), 1_000_000e6);
}
}
Loading
Loading