Skip to content

Commit

Permalink
feat: flow examples (#37)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
andreivladbrg and smol-ninja authored Dec 12, 2024
1 parent 689f633 commit cdee078
Show file tree
Hide file tree
Showing 36 changed files with 515 additions and 20 deletions.
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

0 comments on commit cdee078

Please sign in to comment.