From c108b5426bd3513bec8d57a3f72b8c01459aa159 Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Wed, 2 Oct 2024 00:55:02 +0300 Subject: [PATCH 01/15] 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 --- .gitmodules | 4 + bun.lockb | Bin 42207 -> 42589 bytes flow/FlowCreator.sol | 50 +++++++++ flow/FlowUtilities.sol | 97 ++++++++++++++++++ foundry.toml | 10 +- .../core/LockupDynamicCurvesCreator.sol | 0 .../core/LockupDynamicCurvesCreator.t.sol | 0 .../core/LockupDynamicStreamCreator.sol | 0 .../core/LockupLinearStreamCreator.sol | 0 {v2 => lockup}/core/LockupStreamCreator.t.sol | 0 .../core/LockupTranchedStreamCreator.sol | 0 {v2 => lockup}/core/RecipientHooks.sol | 0 {v2 => lockup}/core/StakeSablierNFT.sol | 0 {v2 => lockup}/core/StreamManagement.sol | 0 .../core/StreamManagementWithHook.sol | 0 .../core/StreamManagementWithHook.t.sol | 0 .../StakeSablierNFT.t.sol | 0 .../claim-rewards/claimRewards.t.sol | 0 .../claim-rewards/claimRewards.tree | 0 .../stake-sablier-nft-test/stake/stake.t.sol | 0 .../stake-sablier-nft-test/stake/stake.tree | 0 .../unstake/unstake.t.sol | 0 .../unstake/unstake.tree | 0 {v2 => lockup}/periphery/AirstreamCreator.sol | 0 .../periphery/AirstreamCreator.t.sol | 0 .../periphery/BatchLDStreamCreator.sol | 0 .../periphery/BatchLLStreamCreator.sol | 0 .../periphery/BatchLTStreamCreator.sol | 0 .../periphery/BatchStreamCreator.t.sol | 0 package.json | 8 +- repos/flow | 1 + 31 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 .gitmodules create mode 100644 flow/FlowCreator.sol create mode 100644 flow/FlowUtilities.sol rename {v2 => lockup}/core/LockupDynamicCurvesCreator.sol (100%) rename {v2 => lockup}/core/LockupDynamicCurvesCreator.t.sol (100%) rename {v2 => lockup}/core/LockupDynamicStreamCreator.sol (100%) rename {v2 => lockup}/core/LockupLinearStreamCreator.sol (100%) rename {v2 => lockup}/core/LockupStreamCreator.t.sol (100%) rename {v2 => lockup}/core/LockupTranchedStreamCreator.sol (100%) rename {v2 => lockup}/core/RecipientHooks.sol (100%) rename {v2 => lockup}/core/StakeSablierNFT.sol (100%) rename {v2 => lockup}/core/StreamManagement.sol (100%) rename {v2 => lockup}/core/StreamManagementWithHook.sol (100%) rename {v2 => lockup}/core/StreamManagementWithHook.t.sol (100%) rename {v2 => lockup}/core/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol (100%) rename {v2 => lockup}/core/tests/stake-sablier-nft-test/claim-rewards/claimRewards.t.sol (100%) rename {v2 => lockup}/core/tests/stake-sablier-nft-test/claim-rewards/claimRewards.tree (100%) rename {v2 => lockup}/core/tests/stake-sablier-nft-test/stake/stake.t.sol (100%) rename {v2 => lockup}/core/tests/stake-sablier-nft-test/stake/stake.tree (100%) rename {v2 => lockup}/core/tests/stake-sablier-nft-test/unstake/unstake.t.sol (100%) rename {v2 => lockup}/core/tests/stake-sablier-nft-test/unstake/unstake.tree (100%) rename {v2 => lockup}/periphery/AirstreamCreator.sol (100%) rename {v2 => lockup}/periphery/AirstreamCreator.t.sol (100%) rename {v2 => lockup}/periphery/BatchLDStreamCreator.sol (100%) rename {v2 => lockup}/periphery/BatchLLStreamCreator.sol (100%) rename {v2 => lockup}/periphery/BatchLTStreamCreator.sol (100%) rename {v2 => lockup}/periphery/BatchStreamCreator.t.sol (100%) create mode 160000 repos/flow diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d66c563 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "repos/flow"] + branch = "main" + path = repos/flow + url = https://github.com/sablier-labs/flow.git diff --git a/bun.lockb b/bun.lockb index d3bf753153b5e4bd4985f8bea89470e49f45c23f..42457e719231391de9890052cb017c19fda31408 100755 GIT binary patch delta 5095 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 OYLXsmVf)$ZN&f{~o|z5+ delta 5053 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 MBYU3u&L5Ni1D)K5&;S4c diff --git a/flow/FlowCreator.sol b/flow/FlowCreator.sol new file mode 100644 index 0000000..86a3ed3 --- /dev/null +++ b/flow/FlowCreator.sol @@ -0,0 +1,50 @@ +// 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 "../repos/flow/src/interfaces/ISablierFlow.sol"; + +import { FlowUtilities } from "./FlowUtilities.sol"; + +contract FlowStreamCreator { + IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + ISablierFlow public immutable sablierFlow; + + constructor(ISablierFlow _sablierFlow) { + sablierFlow = _sablierFlow; + } + + // Create a stream that sends 1000 USDC per month for 30 days + function createStream_1T_PerMonth() external returns (uint256 streamId) { + uint128 amount = 1000e6; + UD21x18 ratePerSecond = + FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: amount, duration: 30 days }); + + streamId = sablierFlow.createAndDeposit({ + 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 + amount: amount // The amount deposited in the stream + }); + } + + // Create a stream that sends 1,000,000 USDC per year + function createStream_1M_PerYear() external returns (uint256 streamId) { + uint128 amount = 1_000_000e6; + UD21x18 ratePerSecond = + FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: amount, duration: 365 days }); + + streamId = sablierFlow.createAndDeposit({ + 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 + amount: amount // The amount deposited in the stream + }); + } +} diff --git a/flow/FlowUtilities.sol b/flow/FlowUtilities.sol new file mode 100644 index 0000000..3a6b195 --- /dev/null +++ b/flow/FlowUtilities.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { ud21x18, UD21x18 } from "@prb/math/src/UD21x18.sol"; + +/// @dev A utility library to calculate the rate per second for a given amount of tokens for a specific duration, and +/// the amounts streamed over a various periods of time. +library FlowUtilities { + /// @notice This function calculates the rate per second for a given amount of tokens for a specific duration. + /// @dev The rate per second is a 18 decimal fixed-point number and it is calculated as `amount / duration`. + /// @param token The address of the token. + /// @param amount The amount of tokens, denoted in token's decimals. + /// @param duration The duration in seconds wished to stream. + /// @return ratePerSecond The rate per second as a fixed-point number. + function ratePerSecondWithDuration( + address token, + uint128 amount, + uint40 duration + ) + internal + view + returns (UD21x18 ratePerSecond) + { + // Get the decimals of the token. + uint8 decimals = IERC20Metadata(token).decimals(); + + // If the token has 18 decimals, we can simply divide the amount by the duration as it returns a 18 decimal + // fixed-point number. + if (decimals == 18) { + return ud21x18(amount / duration); + } + + // Calculate the scale factor from the token's decimals. + uint128 scaleFactor = uint128(10 ** (18 - decimals)); + + // Multiply the amount by the scale factor and divide by the duration. + ratePerSecond = ud21x18(scaleFactor * amount / duration); + } + + /// @notice This function calculates the rate per second for a given amount of tokens for a specific range of time. + /// @dev The rate per second is a 18 decimal fixed-point number and it is calculated as `amount / (end - start)`. + /// @param token The address of the token. + /// @param amount The amount of tokens, denoted in token's decimals. + /// @param start The start timestamp. + /// @param end The end timestamp. + /// @return ratePerSecond The rate per second as a fixed-point number. + function ratePerSecondForTimestamps( + address token, + uint128 amount, + uint40 start, + uint40 end + ) + internal + view + returns (UD21x18 ratePerSecond) + { + // Get the decimals of the token. + uint8 decimals = IERC20Metadata(token).decimals(); + + // Calculate the duration. + uint40 duration = end - start; + + if (decimals < 18) { + return ud21x18(amount / duration); + } + + // Calculate the scale factor from the token's decimals. + uint128 scaleFactor = uint128(10 ** (18 - decimals)); + + // Multiply the amount by the scale factor and divide by the duration. + ratePerSecond = ud21x18(scaleFactor * amount / duration); + } + + /// @notice This function calculates the amount streamed over a week for a given rate per second. + /// @param ratePerSecond The rate per second as a fixed-point number. + /// @return amountPerWeek The amount streamed over a week. + function calculateAmountStreamedPerWeek(UD21x18 ratePerSecond) internal pure returns (uint128 amountPerWeek) { + amountPerWeek = uint128(ratePerSecond.unwrap() / 1 weeks); + } + + /// @notice This function calculates the amount streamed over a month for a given rate per second. + /// @dev We use 30 days as the number of days in a month. + /// @param ratePerSecond The rate per second as a fixed-point number. + /// @return amountPerMonth The amount streamed over a month. + function calculateAmountStreamedPerMonth(UD21x18 ratePerSecond) internal pure returns (uint128 amountPerMonth) { + amountPerMonth = uint128(ratePerSecond.unwrap() / 30 days); + } + + /// @notice This function calculates the amount streamed over a year for a given rate per second. + /// @dev We use 365 days as the number of days in a year. + /// @param ratePerSecond The rate per second as a fixed-point number. + /// @return amountPerYear The amount streamed over a year. + function calculateAmountStreamedPerYear(UD21x18 ratePerSecond) internal pure returns (uint128 amountPerYear) { + amountPerYear = uint128(ratePerSecond.unwrap() / 365 days); + } +} diff --git a/foundry.toml b/foundry.toml index 26fd21e..a71c655 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,8 +8,14 @@ optimizer = true optimizer_runs = 5000 out = "out" solc = "0.8.26" -src = "v2" -test = "v2" + +[profile.lockup] +src = "lockup" +test = "lockup" + +[profile.flow] +src = "flow" +test = "flow" [fmt] bracket_spacing = true diff --git a/v2/core/LockupDynamicCurvesCreator.sol b/lockup/core/LockupDynamicCurvesCreator.sol similarity index 100% rename from v2/core/LockupDynamicCurvesCreator.sol rename to lockup/core/LockupDynamicCurvesCreator.sol diff --git a/v2/core/LockupDynamicCurvesCreator.t.sol b/lockup/core/LockupDynamicCurvesCreator.t.sol similarity index 100% rename from v2/core/LockupDynamicCurvesCreator.t.sol rename to lockup/core/LockupDynamicCurvesCreator.t.sol diff --git a/v2/core/LockupDynamicStreamCreator.sol b/lockup/core/LockupDynamicStreamCreator.sol similarity index 100% rename from v2/core/LockupDynamicStreamCreator.sol rename to lockup/core/LockupDynamicStreamCreator.sol diff --git a/v2/core/LockupLinearStreamCreator.sol b/lockup/core/LockupLinearStreamCreator.sol similarity index 100% rename from v2/core/LockupLinearStreamCreator.sol rename to lockup/core/LockupLinearStreamCreator.sol diff --git a/v2/core/LockupStreamCreator.t.sol b/lockup/core/LockupStreamCreator.t.sol similarity index 100% rename from v2/core/LockupStreamCreator.t.sol rename to lockup/core/LockupStreamCreator.t.sol diff --git a/v2/core/LockupTranchedStreamCreator.sol b/lockup/core/LockupTranchedStreamCreator.sol similarity index 100% rename from v2/core/LockupTranchedStreamCreator.sol rename to lockup/core/LockupTranchedStreamCreator.sol diff --git a/v2/core/RecipientHooks.sol b/lockup/core/RecipientHooks.sol similarity index 100% rename from v2/core/RecipientHooks.sol rename to lockup/core/RecipientHooks.sol diff --git a/v2/core/StakeSablierNFT.sol b/lockup/core/StakeSablierNFT.sol similarity index 100% rename from v2/core/StakeSablierNFT.sol rename to lockup/core/StakeSablierNFT.sol diff --git a/v2/core/StreamManagement.sol b/lockup/core/StreamManagement.sol similarity index 100% rename from v2/core/StreamManagement.sol rename to lockup/core/StreamManagement.sol diff --git a/v2/core/StreamManagementWithHook.sol b/lockup/core/StreamManagementWithHook.sol similarity index 100% rename from v2/core/StreamManagementWithHook.sol rename to lockup/core/StreamManagementWithHook.sol diff --git a/v2/core/StreamManagementWithHook.t.sol b/lockup/core/StreamManagementWithHook.t.sol similarity index 100% rename from v2/core/StreamManagementWithHook.t.sol rename to lockup/core/StreamManagementWithHook.t.sol diff --git a/v2/core/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol b/lockup/core/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol similarity index 100% rename from v2/core/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol rename to lockup/core/tests/stake-sablier-nft-test/StakeSablierNFT.t.sol diff --git a/v2/core/tests/stake-sablier-nft-test/claim-rewards/claimRewards.t.sol b/lockup/core/tests/stake-sablier-nft-test/claim-rewards/claimRewards.t.sol similarity index 100% rename from v2/core/tests/stake-sablier-nft-test/claim-rewards/claimRewards.t.sol rename to lockup/core/tests/stake-sablier-nft-test/claim-rewards/claimRewards.t.sol diff --git a/v2/core/tests/stake-sablier-nft-test/claim-rewards/claimRewards.tree b/lockup/core/tests/stake-sablier-nft-test/claim-rewards/claimRewards.tree similarity index 100% rename from v2/core/tests/stake-sablier-nft-test/claim-rewards/claimRewards.tree rename to lockup/core/tests/stake-sablier-nft-test/claim-rewards/claimRewards.tree diff --git a/v2/core/tests/stake-sablier-nft-test/stake/stake.t.sol b/lockup/core/tests/stake-sablier-nft-test/stake/stake.t.sol similarity index 100% rename from v2/core/tests/stake-sablier-nft-test/stake/stake.t.sol rename to lockup/core/tests/stake-sablier-nft-test/stake/stake.t.sol diff --git a/v2/core/tests/stake-sablier-nft-test/stake/stake.tree b/lockup/core/tests/stake-sablier-nft-test/stake/stake.tree similarity index 100% rename from v2/core/tests/stake-sablier-nft-test/stake/stake.tree rename to lockup/core/tests/stake-sablier-nft-test/stake/stake.tree diff --git a/v2/core/tests/stake-sablier-nft-test/unstake/unstake.t.sol b/lockup/core/tests/stake-sablier-nft-test/unstake/unstake.t.sol similarity index 100% rename from v2/core/tests/stake-sablier-nft-test/unstake/unstake.t.sol rename to lockup/core/tests/stake-sablier-nft-test/unstake/unstake.t.sol diff --git a/v2/core/tests/stake-sablier-nft-test/unstake/unstake.tree b/lockup/core/tests/stake-sablier-nft-test/unstake/unstake.tree similarity index 100% rename from v2/core/tests/stake-sablier-nft-test/unstake/unstake.tree rename to lockup/core/tests/stake-sablier-nft-test/unstake/unstake.tree diff --git a/v2/periphery/AirstreamCreator.sol b/lockup/periphery/AirstreamCreator.sol similarity index 100% rename from v2/periphery/AirstreamCreator.sol rename to lockup/periphery/AirstreamCreator.sol diff --git a/v2/periphery/AirstreamCreator.t.sol b/lockup/periphery/AirstreamCreator.t.sol similarity index 100% rename from v2/periphery/AirstreamCreator.t.sol rename to lockup/periphery/AirstreamCreator.t.sol diff --git a/v2/periphery/BatchLDStreamCreator.sol b/lockup/periphery/BatchLDStreamCreator.sol similarity index 100% rename from v2/periphery/BatchLDStreamCreator.sol rename to lockup/periphery/BatchLDStreamCreator.sol diff --git a/v2/periphery/BatchLLStreamCreator.sol b/lockup/periphery/BatchLLStreamCreator.sol similarity index 100% rename from v2/periphery/BatchLLStreamCreator.sol rename to lockup/periphery/BatchLLStreamCreator.sol diff --git a/v2/periphery/BatchLTStreamCreator.sol b/lockup/periphery/BatchLTStreamCreator.sol similarity index 100% rename from v2/periphery/BatchLTStreamCreator.sol rename to lockup/periphery/BatchLTStreamCreator.sol diff --git a/v2/periphery/BatchStreamCreator.t.sol b/lockup/periphery/BatchStreamCreator.t.sol similarity index 100% rename from v2/periphery/BatchStreamCreator.t.sol rename to lockup/periphery/BatchStreamCreator.t.sol diff --git a/package.json b/package.json index e6554d3..8a328bd 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@openzeppelin/contracts": "5.0.2", - "@prb/math": "4.0.3", + "@prb/math": "github:PaulRBerg/prb-math/#95f00b2", "@sablier/v2-core": "1.2.0", "@sablier/v2-periphery": "1.2.0" }, @@ -40,12 +40,14 @@ "private": true, "repository": "github.com:sablier-labs/examples", "scripts": { - "build": "forge build", + "build:flow": "FOUNDRY_PROFILE=flow forge build", + "build:lockup": "FOUNDRY_PROFILE=lockup forge build", "clean": "rm -rf cache out", "lint": "bun run lint:sol && bun run prettier:check", "lint:sol": "forge fmt --check && bun solhint \"{script,src,test}/**/*.sol\"", "prettier:check": "prettier --check \"**/*.{json,md,yml}\"", "prettier:write": "prettier --write \"**/*.{json,md,yml}\"", - "test": "forge test" + "test:flow": "FOUNDRY_PROFILE=flow forge test", + "test:lockup": "FOUNDRY_PROFILE=lockup forge test" } } diff --git a/repos/flow b/repos/flow new file mode 160000 index 0000000..8d0e464 --- /dev/null +++ b/repos/flow @@ -0,0 +1 @@ +Subproject commit 8d0e464416b2780c784e1a86042af12faac41224 From dbade5ed053aea84e6709da5181015e087d83ec6 Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Thu, 3 Oct 2024 00:12:51 +0300 Subject: [PATCH 02/15] feat: add flow batchable --- .gitmodules | 2 +- flow/FlowBatchable.sol | 146 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 flow/FlowBatchable.sol diff --git a/.gitmodules b/.gitmodules index d66c563..193f023 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "repos/flow"] branch = "main" path = repos/flow - url = https://github.com/sablier-labs/flow.git + url = https://github.com/sablier-labs/flow.git \ No newline at end of file diff --git a/flow/FlowBatchable.sol b/flow/FlowBatchable.sol new file mode 100644 index 0000000..126a722 --- /dev/null +++ b/flow/FlowBatchable.sol @@ -0,0 +1,146 @@ +// 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, UD60x18 } from "@prb/math/src/UD60x18.sol"; + +import { Broker, SablierFlow } from "../repos/flow/src/SablierFlow.sol"; + +/// @dev 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. +contract FlowStreamBatchable { + IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + SablierFlow public immutable sablierFlow; + + constructor(SablierFlow _sablierFlow) { + sablierFlow = _sablierFlow; + } + + /// @dev A function to adjust the rate per second and deposits into a stream. + function adjustRatePerSecondAndDeposit(uint256 streamId) external { + UD21x18 newRatePerSecond = ud21x18(0.0001e18); + uint128 depositAmount = 1000e6; + + // The calldata declared as bytes + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(sablierFlow.adjustRatePerSecond, (streamId, newRatePerSecond)); + calls[1] = abi.encodeCall(sablierFlow.deposit, (streamId, depositAmount)); + + sablierFlow.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 calldata declared as bytes + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(sablierFlow.create, (sender, firstRecipient, firstRatePerSecond, USDC, true)); + calls[1] = abi.encodeCall(sablierFlow.create, (sender, secondRecipient, secondRatePerSecond, USDC, true)); + + // Prepare the `streamIds` array to return them + uint256 nextStreamId = sablierFlow.nextStreamId(); + streamIds = new uint256[](2); + streamIds[0] = nextStreamId; + streamIds[1] = nextStreamId + 1; + + // Execute multiple calls in a single transaction using the prepared call data. + sablierFlow.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 + }); + + streamId = sablierFlow.nextStreamId(); + + // The calldata declared as bytes + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(sablierFlow.create, (sender, recipient, ratePerSecond, USDC, true)); + calls[1] = abi.encodeCall(sablierFlow.depositViaBroker, (streamId, depositAmount, broker)); + + // Execute multiple calls in a single transaction using the prepared call data. + sablierFlow.batch(calls); + } + + /// @dev A function to create multiple streams and deposit via a broker 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; + + // The broker struct + Broker memory broker = Broker({ + account: address(0xDEAD), + fee: ud60x18(0.0001e18) // the fee percentage + }); + + uint256 nextStreamId = sablierFlow.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(sablierFlow.create, (sender, firstRecipient, ratePerSecond, USDC, true)); + calls[1] = abi.encodeCall(sablierFlow.create, (sender, secondRecipient, ratePerSecond, USDC, true)); + calls[2] = abi.encodeCall(sablierFlow.depositViaBroker, (sablierFlow.nextStreamId(), depositAmount, broker)); + calls[3] = abi.encodeCall(sablierFlow.depositViaBroker, (sablierFlow.nextStreamId(), depositAmount, broker)); + + // Execute multiple calls in a single transaction using the prepared call data. + sablierFlow.batch(calls); + } + + /// @dev A function to pause a stream and withdraw the maximum available funds. + function pauseAndWithdrawMax(uint256 streamId) external { + // The calldata declared as bytes + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(sablierFlow.pause, (streamId)); + calls[1] = abi.encodeCall(sablierFlow.withdrawMax, (streamId, address(0xCAFE))); + + // Execute multiple calls in a single transaction using the prepared call data. + sablierFlow.batch(calls); + } + + /// @dev A function to void a stream and withdraw what is left. + function voidAndWithdrawMax(uint256 streamId) external { + // The calldata declared as bytes + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(sablierFlow.void, (streamId)); + calls[1] = abi.encodeCall(sablierFlow.withdrawMax, (streamId, address(0xCAFE))); + + // Execute multiple calls in a single transaction using the prepared call data. + sablierFlow.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 = sablierFlow.getRecipient(streamIds[i]); + calls[i] = abi.encodeCall(sablierFlow.withdrawMax, (streamIds[i], recipient)); + } + + // Execute multiple calls in a single transaction using the prepared call data. + sablierFlow.batch(calls); + } +} From 4b9c31f4f6c838baed9b63180f7fc270237847e4 Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Thu, 3 Oct 2024 00:40:01 +0300 Subject: [PATCH 03/15] feat: add flow manager contract --- flow/FlowCreator.sol | 2 +- flow/FlowManager.sol | 58 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 flow/FlowManager.sol diff --git a/flow/FlowCreator.sol b/flow/FlowCreator.sol index 86a3ed3..ff6d2e6 100644 --- a/flow/FlowCreator.sol +++ b/flow/FlowCreator.sol @@ -16,7 +16,7 @@ contract FlowStreamCreator { sablierFlow = _sablierFlow; } - // Create a stream that sends 1000 USDC per month for 30 days + // Create a stream that sends 1000 USDC per month function createStream_1T_PerMonth() external returns (uint256 streamId) { uint128 amount = 1000e6; UD21x18 ratePerSecond = diff --git a/flow/FlowManager.sol b/flow/FlowManager.sol new file mode 100644 index 0000000..ff9d506 --- /dev/null +++ b/flow/FlowManager.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { ud21x18, UD21x18 } from "@prb/math/src/UD21x18.sol"; + +import { ISablierFlow } from "../repos/flow/src/interfaces/ISablierFlow.sol"; + +contract FlowManager { + ISablierFlow public immutable sablierFlow; + + constructor(ISablierFlow sablierFlow_) { + sablierFlow = sablierFlow_; + } + + function adjustRatePerSecond(uint256 streamId) external { + sablierFlow.adjustRatePerSecond({ streamId: streamId, newRatePerSecond: ud21x18(0.0001e18) }); + } + + function deposit(uint256 streamId) external { + sablierFlow.deposit(streamId, 3.14159e18); + } + + function depositAndPause(uint256 streamId) external { + sablierFlow.depositAndPause(streamId, 3.14159e18); + } + + function pause(uint256 streamId) external { + sablierFlow.pause(streamId); + } + + function refund(uint256 streamId) external { + sablierFlow.refund({ streamId: streamId, amount: 1.61803e18 }); + } + + function refundAndPause(uint256 streamId) external { + sablierFlow.refundAndPause({ streamId: streamId, amount: 1.61803e18 }); + } + + function restart(uint256 streamId) external { + sablierFlow.restart({ streamId: streamId, ratePerSecond: ud21x18(0.0001e18) }); + } + + function restartAndDeposit(uint256 streamId) external { + sablierFlow.restartAndDeposit({ streamId: streamId, ratePerSecond: ud21x18(0.0001e18), amount: 2.71828e18 }); + } + + function void(uint256 streamId) external { + sablierFlow.void(streamId); + } + + function withdraw(uint256 streamId) external { + sablierFlow.withdraw({ streamId: streamId, to: address(0xCAFE), amount: 2.71828e18 }); + } + + function withdrawMax(uint256 streamId) external { + sablierFlow.withdrawMax({ streamId: streamId, to: address(0xCAFE) }); + } +} From 492e893cffced8b2c2a4eaaeb4d3efa2c50ca4bc Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Thu, 3 Oct 2024 00:44:42 +0300 Subject: [PATCH 04/15] fix: calculation --- flow/FlowUtilities.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flow/FlowUtilities.sol b/flow/FlowUtilities.sol index 3a6b195..f9bcccf 100644 --- a/flow/FlowUtilities.sol +++ b/flow/FlowUtilities.sol @@ -4,8 +4,8 @@ pragma solidity >=0.8.22; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { ud21x18, UD21x18 } from "@prb/math/src/UD21x18.sol"; -/// @dev A utility library to calculate the rate per second for a given amount of tokens for a specific duration, and -/// the amounts streamed over a various periods of time. +/// @dev A utility library to calculate the rate per second for a given amount of tokens over a specific duration, and +/// the amounts streamed over various periods of time. library FlowUtilities { /// @notice This function calculates the rate per second for a given amount of tokens for a specific duration. /// @dev The rate per second is a 18 decimal fixed-point number and it is calculated as `amount / duration`. @@ -76,7 +76,7 @@ library FlowUtilities { /// @param ratePerSecond The rate per second as a fixed-point number. /// @return amountPerWeek The amount streamed over a week. function calculateAmountStreamedPerWeek(UD21x18 ratePerSecond) internal pure returns (uint128 amountPerWeek) { - amountPerWeek = uint128(ratePerSecond.unwrap() / 1 weeks); + amountPerWeek = ratePerSecond.unwrap() * 1 weeks; } /// @notice This function calculates the amount streamed over a month for a given rate per second. @@ -84,7 +84,7 @@ library FlowUtilities { /// @param ratePerSecond The rate per second as a fixed-point number. /// @return amountPerMonth The amount streamed over a month. function calculateAmountStreamedPerMonth(UD21x18 ratePerSecond) internal pure returns (uint128 amountPerMonth) { - amountPerMonth = uint128(ratePerSecond.unwrap() / 30 days); + amountPerMonth = ratePerSecond.unwrap() * 30 days; } /// @notice This function calculates the amount streamed over a year for a given rate per second. @@ -92,6 +92,6 @@ library FlowUtilities { /// @param ratePerSecond The rate per second as a fixed-point number. /// @return amountPerYear The amount streamed over a year. function calculateAmountStreamedPerYear(UD21x18 ratePerSecond) internal pure returns (uint128 amountPerYear) { - amountPerYear = uint128(ratePerSecond.unwrap() / 365 days); + amountPerYear = ratePerSecond.unwrap() * 365 days; } } From 1436fd0a0a170c905442a6c57ada3f2aa2eecf43 Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Thu, 3 Oct 2024 16:40:09 +0300 Subject: [PATCH 05/15] correct contract name --- flow/FlowBatchable.sol | 2 +- flow/{FlowCreator.sol => FlowStreamCreator.sol} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename flow/{FlowCreator.sol => FlowStreamCreator.sol} (100%) diff --git a/flow/FlowBatchable.sol b/flow/FlowBatchable.sol index 126a722..171a6ef 100644 --- a/flow/FlowBatchable.sol +++ b/flow/FlowBatchable.sol @@ -9,7 +9,7 @@ import { Broker, SablierFlow } from "../repos/flow/src/SablierFlow.sol"; /// @dev 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. -contract FlowStreamBatchable { +contract FlowBatchable { IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); SablierFlow public immutable sablierFlow; diff --git a/flow/FlowCreator.sol b/flow/FlowStreamCreator.sol similarity index 100% rename from flow/FlowCreator.sol rename to flow/FlowStreamCreator.sol From b5b285a77d6f2c38cc912b7d88ee5ff43520a6d1 Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Thu, 3 Oct 2024 16:47:46 +0300 Subject: [PATCH 06/15] rename constructor var --- flow/FlowBatchable.sol | 4 ++-- flow/FlowStreamCreator.sol | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flow/FlowBatchable.sol b/flow/FlowBatchable.sol index 171a6ef..986e200 100644 --- a/flow/FlowBatchable.sol +++ b/flow/FlowBatchable.sol @@ -13,8 +13,8 @@ contract FlowBatchable { IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); SablierFlow public immutable sablierFlow; - constructor(SablierFlow _sablierFlow) { - sablierFlow = _sablierFlow; + constructor(SablierFlow sablierFlow_) { + sablierFlow = sablierFlow_; } /// @dev A function to adjust the rate per second and deposits into a stream. diff --git a/flow/FlowStreamCreator.sol b/flow/FlowStreamCreator.sol index ff6d2e6..9443dd7 100644 --- a/flow/FlowStreamCreator.sol +++ b/flow/FlowStreamCreator.sol @@ -12,8 +12,8 @@ contract FlowStreamCreator { IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); ISablierFlow public immutable sablierFlow; - constructor(ISablierFlow _sablierFlow) { - sablierFlow = _sablierFlow; + constructor(ISablierFlow sablierFlow_) { + sablierFlow = sablierFlow_; } // Create a stream that sends 1000 USDC per month From d749bb2b75689609d6ccdb05f3e6ddfce4f263de Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Thu, 3 Oct 2024 17:57:29 +0300 Subject: [PATCH 07/15] fix spelling --- flow/FlowBatchable.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flow/FlowBatchable.sol b/flow/FlowBatchable.sol index 986e200..2006c4f 100644 --- a/flow/FlowBatchable.sol +++ b/flow/FlowBatchable.sol @@ -22,7 +22,7 @@ contract FlowBatchable { UD21x18 newRatePerSecond = ud21x18(0.0001e18); uint128 depositAmount = 1000e6; - // The calldata declared as bytes + // The call data declared as bytes bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(sablierFlow.adjustRatePerSecond, (streamId, newRatePerSecond)); calls[1] = abi.encodeCall(sablierFlow.deposit, (streamId, depositAmount)); @@ -38,7 +38,7 @@ contract FlowBatchable { UD21x18 firstRatePerSecond = ud21x18(0.0001e18); UD21x18 secondRatePerSecond = ud21x18(0.0002e18); - // The calldata declared as bytes + // The call data declared as bytes bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(sablierFlow.create, (sender, firstRecipient, firstRatePerSecond, USDC, true)); calls[1] = abi.encodeCall(sablierFlow.create, (sender, secondRecipient, secondRatePerSecond, USDC, true)); @@ -68,7 +68,7 @@ contract FlowBatchable { streamId = sablierFlow.nextStreamId(); - // The calldata declared as bytes + // The call data declared as bytes bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(sablierFlow.create, (sender, recipient, ratePerSecond, USDC, true)); calls[1] = abi.encodeCall(sablierFlow.depositViaBroker, (streamId, depositAmount, broker)); @@ -109,7 +109,7 @@ contract FlowBatchable { /// @dev A function to pause a stream and withdraw the maximum available funds. function pauseAndWithdrawMax(uint256 streamId) external { - // The calldata declared as bytes + // The call data declared as bytes bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(sablierFlow.pause, (streamId)); calls[1] = abi.encodeCall(sablierFlow.withdrawMax, (streamId, address(0xCAFE))); @@ -120,7 +120,7 @@ contract FlowBatchable { /// @dev A function to void a stream and withdraw what is left. function voidAndWithdrawMax(uint256 streamId) external { - // The calldata declared as bytes + // The call data declared as bytes bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(sablierFlow.void, (streamId)); calls[1] = abi.encodeCall(sablierFlow.withdrawMax, (streamId, address(0xCAFE))); From 8bb3f4cb582c85028f6de691ca8392691c8ba03d Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Tue, 22 Oct 2024 13:22:43 +0300 Subject: [PATCH 08/15] fix: deposit functions build: install flow repo via package.json test: add tests for FlowStreamCreator test: add tests for FlowBatchable --- .gitmodules | 4 -- bun.lockb | Bin 42589 -> 43019 bytes flow/FlowBatchable.sol | 81 ++++++++++++++++++++++------------- flow/FlowBatchable.t.sol | 58 +++++++++++++++++++++++++ flow/FlowManager.sol | 4 +- flow/FlowStreamCreator.sol | 21 ++++----- flow/FlowStreamCreator.t.sol | 60 ++++++++++++++++++++++++++ package.json | 3 +- remappings.txt | 1 + repos/flow | 1 - 10 files changed, 183 insertions(+), 50 deletions(-) delete mode 100644 .gitmodules create mode 100644 flow/FlowBatchable.t.sol create mode 100644 flow/FlowStreamCreator.t.sol delete mode 160000 repos/flow diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 193f023..0000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "repos/flow"] - branch = "main" - path = repos/flow - url = https://github.com/sablier-labs/flow.git \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 42457e719231391de9890052cb017c19fda31408..f145dcdd909d7147736c840000e6f61ea0fc7c05 100755 GIT binary patch delta 8182 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.8.22; + +import { Test } from "forge-std/src/Test.sol"; + +import { FlowBatchable } from "./FlowBatchable.sol"; + +import { IFlowNFTDescriptor, SablierFlow } from "@sablier/flow/src/SablierFlow.sol"; + +contract FlowBatchable_Test is Test { + FlowBatchable internal batchable; + SablierFlow internal flow; + address internal user; + + function setUp() external { + // Fork Ethereum Sepolia + vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 6_240_816 }); + + // Deploy a SablierFlow contract + flow = new SablierFlow({ initialAdmin: address(this), initialNFTDescriptor: IFlowNFTDescriptor(address(this)) }); + + // Deploy the batchable contract + batchable = new FlowBatchable(flow); + + 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 = 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 = flow.nextStreamId(); + + uint256[] memory actualStreamIds = batchable.createMultipleAndDepositViaBroker(); + uint256[] memory expectedStreamIds = new uint256[](2); + expectedStreamIds[0] = nextStreamIdBefore; + expectedStreamIds[1] = nextStreamIdBefore + 1; + + assertEq(actualStreamIds, expectedStreamIds); + } +} diff --git a/flow/FlowManager.sol b/flow/FlowManager.sol index ff9d506..9aa2418 100644 --- a/flow/FlowManager.sol +++ b/flow/FlowManager.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.22; import { ud21x18, UD21x18 } from "@prb/math/src/UD21x18.sol"; -import { ISablierFlow } from "../repos/flow/src/interfaces/ISablierFlow.sol"; +import { ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol"; contract FlowManager { ISablierFlow public immutable sablierFlow; @@ -17,7 +17,7 @@ contract FlowManager { } function deposit(uint256 streamId) external { - sablierFlow.deposit(streamId, 3.14159e18); + sablierFlow.deposit(streamId, 3.14159e18, msg.sender, address(0xCAFE)); } function depositAndPause(uint256 streamId) external { diff --git a/flow/FlowStreamCreator.sol b/flow/FlowStreamCreator.sol index 9443dd7..e62ca8e 100644 --- a/flow/FlowStreamCreator.sol +++ b/flow/FlowStreamCreator.sol @@ -4,12 +4,13 @@ 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 "../repos/flow/src/interfaces/ISablierFlow.sol"; +import { ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol"; import { FlowUtilities } from "./FlowUtilities.sol"; contract FlowStreamCreator { - IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + // Mainnet USDC + IERC20 public constant USDC = IERC20(0xf08A50178dfcDe18524640EA6618a1f965821715); ISablierFlow public immutable sablierFlow; constructor(ISablierFlow sablierFlow_) { @@ -18,33 +19,29 @@ contract FlowStreamCreator { // Create a stream that sends 1000 USDC per month function createStream_1T_PerMonth() external returns (uint256 streamId) { - uint128 amount = 1000e6; UD21x18 ratePerSecond = - FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: amount, duration: 30 days }); + FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: 1000e6, duration: 30 days }); - streamId = sablierFlow.createAndDeposit({ + streamId = sablierFlow.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 - amount: amount // The amount deposited in the stream + 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) { - uint128 amount = 1_000_000e6; UD21x18 ratePerSecond = - FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: amount, duration: 365 days }); + FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: 1_000_000e6, duration: 365 days }); - streamId = sablierFlow.createAndDeposit({ + streamId = sablierFlow.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 - amount: amount // The amount deposited in the stream + transferable: true // Whether the stream will be transferable or not }); } } diff --git a/flow/FlowStreamCreator.t.sol b/flow/FlowStreamCreator.t.sol new file mode 100644 index 0000000..bdc3e62 --- /dev/null +++ b/flow/FlowStreamCreator.t.sol @@ -0,0 +1,60 @@ +// 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"; + +import { IFlowNFTDescriptor, SablierFlow } from "@sablier/flow/src/SablierFlow.sol"; + +contract FlowStreamCreator_Test is Test { + FlowStreamCreator internal streamCreator; + SablierFlow internal flow; + address internal user; + + function setUp() external { + // Fork Ethereum Sepolia + vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 6_240_816 }); + + // Deploy a SablierFlow contract + flow = new SablierFlow({ initialAdmin: address(this), initialNFTDescriptor: IFlowNFTDescriptor(address(this)) }); + + // Deploy the FlowStreamCreator contract + streamCreator = new FlowStreamCreator(flow); + + 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_1T_PerMonth() external { + uint256 expectedStreamId = flow.nextStreamId(); + + uint256 actualStreamId = streamCreator.createStream_1T_PerMonth(); + assertEq(actualStreamId, expectedStreamId); + + // Warp more than 30 days into the future to see if the debt accumulated is more than 1 thousand + vm.warp({ newTimestamp: block.timestamp + 30 days + 1 seconds }); + + assertGe(flow.totalDebtOf(actualStreamId), 1000e6); + } + + function test_CreateStream_1M_PerYear() external { + uint256 expectedStreamId = flow.nextStreamId(); + + uint256 actualStreamId = streamCreator.createStream_1M_PerYear(); + assertEq(actualStreamId, expectedStreamId); + + // Warp more than 30 days into the future to see if the debt accumulated is more than 1 thousand + vm.warp({ newTimestamp: block.timestamp + 365 days + 1 seconds }); + + assertGe(flow.totalDebtOf(actualStreamId), 1_000_000e6); + } +} diff --git a/package.json b/package.json index 8a328bd..657b66e 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ }, "dependencies": { "@openzeppelin/contracts": "5.0.2", - "@prb/math": "github:PaulRBerg/prb-math/#95f00b2", + "@prb/math": "4.1.0", + "@sablier/flow": "github:sablier-labs/flow#main", "@sablier/v2-core": "1.2.0", "@sablier/v2-periphery": "1.2.0" }, diff --git a/remappings.txt b/remappings.txt index 86e658e..8331950 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,5 +1,6 @@ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ @prb/math/=node_modules/@prb/math/ +@sablier/flow/=node_modules/@sablier/flow/ @sablier/v2-core/=node_modules/@sablier/v2-core/ @sablier/v2-periphery/=node_modules/@sablier/v2-periphery/ forge-std/=node_modules/forge-std/ diff --git a/repos/flow b/repos/flow deleted file mode 160000 index 8d0e464..0000000 --- a/repos/flow +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8d0e464416b2780c784e1a86042af12faac41224 From 9fedb7ad0f2ce57842e8b6e3da3999c39b935e7d Mon Sep 17 00:00:00 2001 From: smol-ninja Date: Tue, 26 Nov 2024 00:16:16 +0000 Subject: [PATCH 09/15] chore: polish flow examples --- .solhint.json | 6 +- bun.lockb | Bin 43019 -> 43014 bytes flow/FlowBatchable.sol | 133 +++++++++++---------- flow/FlowManager.sol | 2 +- flow/FlowStreamCreator.sol | 6 +- flow/FlowStreamCreator.t.sol | 8 +- flow/FlowUtilities.sol | 31 +++-- lockup/core/RecipientHooks.sol | 2 +- lockup/core/StreamManagementWithHook.t.sol | 12 +- package.json | 11 +- 10 files changed, 109 insertions(+), 102 deletions(-) diff --git a/.solhint.json b/.solhint.json index 0b9101b..7620360 100644 --- a/.solhint.json +++ b/.solhint.json @@ -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" } } diff --git a/bun.lockb b/bun.lockb index f145dcdd909d7147736c840000e6f61ea0fc7c05..20396e907af08d288f5a4dc1031e3a791e23d2f6 100755 GIT binary patch delta 4151 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.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ud21x18, UD21x18 } from "@prb/math/src/UD21x18.sol"; -import { ud60x18, UD60x18 } from "@prb/math/src/UD60x18.sol"; +import { ud60x18 } from "@prb/math/src/UD60x18.sol"; import { Broker, SablierFlow } from "@sablier/flow/src/SablierFlow.sol"; -/// @dev 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. +/// @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 { IERC20 public constant USDC = IERC20(0xf08A50178dfcDe18524640EA6618a1f965821715); - SablierFlow public immutable SABLIER_FLOW; + SablierFlow public immutable sablierFlow; constructor(SablierFlow sablierFlow_) { - SABLIER_FLOW = sablierFlow_; + sablierFlow = sablierFlow_; } - /// @dev A function to adjust the rate per second and deposits into a stream. + /// @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 both streams. + // 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(SABLIER_FLOW), depositAmount); + // Approve the Sablier contract to spend USDC. + USDC.approve(address(sablierFlow), depositAmount); - // The call data declared as bytes - bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeCall(SABLIER_FLOW.adjustRatePerSecond, (streamId, newRatePerSecond)); - calls[1] = abi.encodeCall(SABLIER_FLOW.deposit, (streamId, depositAmount, msg.sender, address(0xCAFE))); - - SABLIER_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); + // Fetch the stream recipient. + address recipient = sablierFlow.getRecipient(streamId); - // The call data declared as bytes + // The call data declared as bytes. bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeCall(SABLIER_FLOW.create, (sender, firstRecipient, firstRatePerSecond, USDC, true)); - calls[1] = abi.encodeCall(SABLIER_FLOW.create, (sender, secondRecipient, secondRatePerSecond, USDC, true)); - - // Prepare the `streamIds` array to return them - uint256 nextStreamId = SABLIER_FLOW.nextStreamId(); - streamIds = new uint256[](2); - streamIds[0] = nextStreamId; - streamIds[1] = nextStreamId + 1; + calls[0] = abi.encodeCall(sablierFlow.adjustRatePerSecond, (streamId, newRatePerSecond)); + calls[1] = abi.encodeCall(sablierFlow.deposit, (streamId, depositAmount, msg.sender, recipient)); - // Execute multiple calls in a single transaction using the prepared call data. - SABLIER_FLOW.batch(calls); + sablierFlow.batch(calls); } /// @dev A function to create a stream and deposit via a broker in a single transaction. @@ -66,30 +47,53 @@ contract FlowBatchable { UD21x18 ratePerSecond = ud21x18(0.0001e18); uint128 depositAmount = 1000e6; - // The broker struct + // 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 both streams. + // 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(SABLIER_FLOW), depositAmount); + // Approve the Sablier contract to spend USDC. + USDC.approve(address(sablierFlow), depositAmount); - streamId = SABLIER_FLOW.nextStreamId(); + streamId = sablierFlow.nextStreamId(); // The call data declared as bytes bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeCall(SABLIER_FLOW.create, (sender, recipient, ratePerSecond, USDC, true)); - calls[1] = abi.encodeCall(SABLIER_FLOW.depositViaBroker, (streamId, depositAmount, sender, recipient, broker)); + calls[0] = abi.encodeCall(sablierFlow.create, (sender, recipient, ratePerSecond, USDC, true)); + calls[1] = abi.encodeCall(sablierFlow.depositViaBroker, (streamId, depositAmount, sender, recipient, broker)); // Execute multiple calls in a single transaction using the prepared call data. - SABLIER_FLOW.batch(calls); + sablierFlow.batch(calls); } - /// @dev A function to create multiple streams and deposit via a broker in a single transaction. + /// @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(sablierFlow.create, (sender, firstRecipient, firstRatePerSecond, USDC, true)); + calls[1] = abi.encodeCall(sablierFlow.create, (sender, secondRecipient, secondRatePerSecond, USDC, true)); + + // Prepare the `streamIds` array to return them + uint256 nextStreamId = sablierFlow.nextStreamId(); + streamIds = new uint256[](2); + streamIds[0] = nextStreamId; + streamIds[1] = nextStreamId + 1; + + // Execute multiple calls in a single transaction using the prepared call data. + sablierFlow.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); @@ -100,8 +104,8 @@ contract FlowBatchable { // 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(SABLIER_FLOW), 2 * depositAmount); + // Approve the Sablier contract to spend USDC. + USDC.approve(address(sablierFlow), 2 * depositAmount); // The broker struct Broker memory broker = Broker({ @@ -109,59 +113,58 @@ contract FlowBatchable { fee: ud60x18(0.0001e18) // the fee percentage }); - uint256 nextStreamId = SABLIER_FLOW.nextStreamId(); + uint256 nextStreamId = sablierFlow.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(SABLIER_FLOW.create, (sender, firstRecipient, ratePerSecond, USDC, true)); - calls[1] = abi.encodeCall(SABLIER_FLOW.create, (sender, secondRecipient, ratePerSecond, USDC, true)); + calls[0] = abi.encodeCall(sablierFlow.create, (sender, firstRecipient, ratePerSecond, USDC, true)); + calls[1] = abi.encodeCall(sablierFlow.create, (sender, secondRecipient, ratePerSecond, USDC, true)); calls[2] = - abi.encodeCall(SABLIER_FLOW.depositViaBroker, (streamIds[0], depositAmount, sender, firstRecipient, broker)); - calls[3] = abi.encodeCall( - SABLIER_FLOW.depositViaBroker, (streamIds[1], depositAmount, sender, secondRecipient, broker) - ); + abi.encodeCall(sablierFlow.depositViaBroker, (streamIds[0], depositAmount, sender, firstRecipient, broker)); + calls[3] = + abi.encodeCall(sablierFlow.depositViaBroker, (streamIds[1], depositAmount, sender, secondRecipient, broker)); // Execute multiple calls in a single transaction using the prepared call data. - SABLIER_FLOW.batch(calls); + sablierFlow.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 + // The call data declared as bytes. bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeCall(SABLIER_FLOW.pause, (streamId)); - calls[1] = abi.encodeCall(SABLIER_FLOW.withdrawMax, (streamId, address(0xCAFE))); + calls[0] = abi.encodeCall(sablierFlow.pause, (streamId)); + calls[1] = abi.encodeCall(sablierFlow.withdrawMax, (streamId, address(0xCAFE))); // Execute multiple calls in a single transaction using the prepared call data. - SABLIER_FLOW.batch(calls); + sablierFlow.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(SABLIER_FLOW.void, (streamId)); - calls[1] = abi.encodeCall(SABLIER_FLOW.withdrawMax, (streamId, address(0xCAFE))); + calls[0] = abi.encodeCall(sablierFlow.void, (streamId)); + calls[1] = abi.encodeCall(sablierFlow.withdrawMax, (streamId, address(0xCAFE))); // Execute multiple calls in a single transaction using the prepared call data. - SABLIER_FLOW.batch(calls); + sablierFlow.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 + // 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 = SABLIER_FLOW.getRecipient(streamIds[i]); - calls[i] = abi.encodeCall(SABLIER_FLOW.withdrawMax, (streamIds[i], recipient)); + address recipient = sablierFlow.getRecipient(streamIds[i]); + calls[i] = abi.encodeCall(sablierFlow.withdrawMax, (streamIds[i], recipient)); } // Execute multiple calls in a single transaction using the prepared call data. - SABLIER_FLOW.batch(calls); + sablierFlow.batch(calls); } } diff --git a/flow/FlowManager.sol b/flow/FlowManager.sol index 9aa2418..11d6af4 100644 --- a/flow/FlowManager.sol +++ b/flow/FlowManager.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; -import { ud21x18, UD21x18 } from "@prb/math/src/UD21x18.sol"; +import { ud21x18 } from "@prb/math/src/UD21x18.sol"; import { ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol"; diff --git a/flow/FlowStreamCreator.sol b/flow/FlowStreamCreator.sol index e62ca8e..977f411 100644 --- a/flow/FlowStreamCreator.sol +++ b/flow/FlowStreamCreator.sol @@ -17,8 +17,8 @@ contract FlowStreamCreator { sablierFlow = sablierFlow_; } - // Create a stream that sends 1000 USDC per month - function createStream_1T_PerMonth() external returns (uint256 streamId) { + // 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 }); @@ -31,7 +31,7 @@ contract FlowStreamCreator { }); } - // Create a stream that sends 1,000,000 USDC per year + // 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 }); diff --git a/flow/FlowStreamCreator.t.sol b/flow/FlowStreamCreator.t.sol index bdc3e62..2d11669 100644 --- a/flow/FlowStreamCreator.t.sol +++ b/flow/FlowStreamCreator.t.sol @@ -34,13 +34,13 @@ contract FlowStreamCreator_Test is Test { streamCreator.USDC().approve({ spender: address(streamCreator), value: 1_000_000e6 }); } - function test_CreateStream_1T_PerMonth() external { + function test_CreateStream_1K_PerMonth() external { uint256 expectedStreamId = flow.nextStreamId(); - uint256 actualStreamId = streamCreator.createStream_1T_PerMonth(); + uint256 actualStreamId = streamCreator.createStream_1K_PerMonth(); assertEq(actualStreamId, expectedStreamId); - // Warp more than 30 days into the future to see if the debt accumulated is more than 1 thousand + // 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(flow.totalDebtOf(actualStreamId), 1000e6); @@ -52,7 +52,7 @@ contract FlowStreamCreator_Test is Test { uint256 actualStreamId = streamCreator.createStream_1M_PerYear(); assertEq(actualStreamId, expectedStreamId); - // Warp more than 30 days into the future to see if the debt accumulated is more than 1 thousand + // 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(flow.totalDebtOf(actualStreamId), 1_000_000e6); diff --git a/flow/FlowUtilities.sol b/flow/FlowUtilities.sol index f9bcccf..caf1cde 100644 --- a/flow/FlowUtilities.sol +++ b/flow/FlowUtilities.sol @@ -4,15 +4,14 @@ pragma solidity >=0.8.22; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { ud21x18, UD21x18 } from "@prb/math/src/UD21x18.sol"; -/// @dev A utility library to calculate the rate per second for a given amount of tokens over a specific duration, and -/// the amounts streamed over various periods of time. +/// @dev A utility library to calculate rate per second and streamed amount based on a given time frame. library FlowUtilities { - /// @notice This function calculates the rate per second for a given amount of tokens for a specific duration. - /// @dev The rate per second is a 18 decimal fixed-point number and it is calculated as `amount / duration`. + /// @notice This function calculates the rate per second based on a given amount of tokens and a specified duration. + /// @dev The rate per second is a 18-decimal fixed-point number and it is calculated as `amount / duration`. /// @param token The address of the token. /// @param amount The amount of tokens, denoted in token's decimals. - /// @param duration The duration in seconds wished to stream. - /// @return ratePerSecond The rate per second as a fixed-point number. + /// @param duration The duration in seconds user wishes to stream. + /// @return ratePerSecond The rate per second as a 18-decimal fixed-point number. function ratePerSecondWithDuration( address token, uint128 amount, @@ -35,16 +34,16 @@ library FlowUtilities { uint128 scaleFactor = uint128(10 ** (18 - decimals)); // Multiply the amount by the scale factor and divide by the duration. - ratePerSecond = ud21x18(scaleFactor * amount / duration); + ratePerSecond = ud21x18((scaleFactor * amount) / duration); } - /// @notice This function calculates the rate per second for a given amount of tokens for a specific range of time. - /// @dev The rate per second is a 18 decimal fixed-point number and it is calculated as `amount / (end - start)`. + /// @notice This function calculates the rate per second based on a given amount of tokens and a specified range. + /// @dev The rate per second is a 18-decimal fixed-point number and it is calculated as `amount / (end - start)`. /// @param token The address of the token. /// @param amount The amount of tokens, denoted in token's decimals. /// @param start The start timestamp. /// @param end The end timestamp. - /// @return ratePerSecond The rate per second as a fixed-point number. + /// @return ratePerSecond The rate per second as a 18-decimal fixed-point number. function ratePerSecondForTimestamps( address token, uint128 amount, @@ -61,7 +60,7 @@ library FlowUtilities { // Calculate the duration. uint40 duration = end - start; - if (decimals < 18) { + if (decimals == 18) { return ud21x18(amount / duration); } @@ -69,26 +68,26 @@ library FlowUtilities { uint128 scaleFactor = uint128(10 ** (18 - decimals)); // Multiply the amount by the scale factor and divide by the duration. - ratePerSecond = ud21x18(scaleFactor * amount / duration); + ratePerSecond = ud21x18((scaleFactor * amount) / duration); } /// @notice This function calculates the amount streamed over a week for a given rate per second. - /// @param ratePerSecond The rate per second as a fixed-point number. + /// @param ratePerSecond The rate per second as a 18-decimal fixed-point number. /// @return amountPerWeek The amount streamed over a week. function calculateAmountStreamedPerWeek(UD21x18 ratePerSecond) internal pure returns (uint128 amountPerWeek) { amountPerWeek = ratePerSecond.unwrap() * 1 weeks; } /// @notice This function calculates the amount streamed over a month for a given rate per second. - /// @dev We use 30 days as the number of days in a month. - /// @param ratePerSecond The rate per second as a fixed-point number. + /// @dev For simplicity, we have assumed that there are 30 days in a month. + /// @param ratePerSecond The rate per second as a 18-decimal fixed-point number. /// @return amountPerMonth The amount streamed over a month. function calculateAmountStreamedPerMonth(UD21x18 ratePerSecond) internal pure returns (uint128 amountPerMonth) { amountPerMonth = ratePerSecond.unwrap() * 30 days; } /// @notice This function calculates the amount streamed over a year for a given rate per second. - /// @dev We use 365 days as the number of days in a year. + /// @dev For simplicity, we have assumed that there are 365 days in a year. /// @param ratePerSecond The rate per second as a fixed-point number. /// @return amountPerYear The amount streamed over a year. function calculateAmountStreamedPerYear(UD21x18 ratePerSecond) internal pure returns (uint128 amountPerYear) { diff --git a/lockup/core/RecipientHooks.sol b/lockup/core/RecipientHooks.sol index 52f038d..bbfdd02 100644 --- a/lockup/core/RecipientHooks.sol +++ b/lockup/core/RecipientHooks.sol @@ -11,7 +11,7 @@ contract RecipientHooks is ISablierLockupRecipient { /// depending on which type of streams are supported in this hook. address public immutable SABLIER_LOCKUP; - mapping(address => uint256) internal _balances; + mapping(address account => uint256 amount) internal _balances; /// @dev Constructor will set the address of the lockup contract. constructor(address sablierLockup_) { diff --git a/lockup/core/StreamManagementWithHook.t.sol b/lockup/core/StreamManagementWithHook.t.sol index c3a9bdc..25cf8c5 100644 --- a/lockup/core/StreamManagementWithHook.t.sol +++ b/lockup/core/StreamManagementWithHook.t.sol @@ -20,7 +20,7 @@ contract StreamManagementWithHookTest is Test { ERC20 internal token; uint128 internal amount = 10e18; - uint256 internal DEFAULT_STREAM_ID; + uint256 internal defaultStreamId; address internal alice; address internal bob; @@ -80,8 +80,8 @@ contract StreamManagementWithHookTest is Test { modifier givenStreamsCreated() { // Create a stream with Alice as the beneficiary - DEFAULT_STREAM_ID = streamManager.create({ beneficiary: alice, totalAmount: amount }); - require(DEFAULT_STREAM_ID == 1, "Stream creation failed"); + defaultStreamId = streamManager.create({ beneficiary: alice, totalAmount: amount }); + require(defaultStreamId == 1, "Stream creation failed"); _; } @@ -95,7 +95,7 @@ contract StreamManagementWithHookTest is Test { // Since Alice is the `msg.sender`, `withdraw` to Sablier stream should revert due to hook restriction vm.expectRevert(abi.encodeWithSelector(StreamManagementWithHook.CallerNotThisContract.selector)); - sablierLockup.withdraw(DEFAULT_STREAM_ID, address(streamManager), 1e18); + sablierLockup.withdraw(defaultStreamId, address(streamManager), 1e18); } // Test that withdraw from Sablier stream succeeds if it is called through the `streamManager` contract @@ -107,12 +107,12 @@ contract StreamManagementWithHookTest is Test { vm.startPrank(alice); // Alice can withdraw from the streamManager contract - streamManager.withdraw(DEFAULT_STREAM_ID, 1e18); + streamManager.withdraw(defaultStreamId, 1e18); assertEq(token.balanceOf(alice), 1e18); // Withdraw max tokens from the stream - streamManager.withdrawMax(DEFAULT_STREAM_ID); + streamManager.withdrawMax(defaultStreamId); assertEq(token.balanceOf(alice), 10e18); } diff --git a/package.json b/package.json index 657b66e..d12f5a8 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,12 @@ "devDependencies": { "forge-std": "github:foundry-rs/forge-std#v1.8.1", "prettier": "^2.8.8", - "solhint": "^4.0.0" + "solhint": "^5.0.3" }, "dependencies": { "@openzeppelin/contracts": "5.0.2", "@prb/math": "4.1.0", - "@sablier/flow": "github:sablier-labs/flow#main", + "@sablier/flow": "github:sablier-labs/flow", "@sablier/v2-core": "1.2.0", "@sablier/v2-periphery": "1.2.0" }, @@ -45,9 +45,10 @@ "build:lockup": "FOUNDRY_PROFILE=lockup forge build", "clean": "rm -rf cache out", "lint": "bun run lint:sol && bun run prettier:check", - "lint:sol": "forge fmt --check && bun solhint \"{script,src,test}/**/*.sol\"", - "prettier:check": "prettier --check \"**/*.{json,md,yml}\"", - "prettier:write": "prettier --write \"**/*.{json,md,yml}\"", + "lint:sol": "forge fmt . && bun solhint \"{flow,lockup}/**/*.sol\"", + "prettier:check": "prettier --check \"**/*.{md,yml}\"", + "prettier:write": "prettier --write \"**/*.{md,yml}\"", + "test": "bun run test:flow && bun run test:lockup", "test:flow": "FOUNDRY_PROFILE=flow forge test", "test:lockup": "FOUNDRY_PROFILE=lockup forge test" } From 19de2aaa58277a5f18da3d27dcc7e60cf6ce1a74 Mon Sep 17 00:00:00 2001 From: smol-ninja Date: Tue, 26 Nov 2024 00:28:25 +0000 Subject: [PATCH 10/15] ci: fix build and test commands --- .github/workflows/ci.yml | 6 +++--- package.json | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7772db4..3702bde 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: | @@ -78,7 +78,7 @@ jobs: run: "forge config" - name: "Run the tests" - run: "forge test" + run: "bun run test" - name: "Add test summary" run: | diff --git a/package.json b/package.json index d12f5a8..0358315 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "private": true, "repository": "github.com:sablier-labs/examples", "scripts": { + "build": "bun run build:flow && bun run build:lockup", "build:flow": "FOUNDRY_PROFILE=flow forge build", "build:lockup": "FOUNDRY_PROFILE=lockup forge build", "clean": "rm -rf cache out", From d0d6630ccebcf872a459a7efbca06fe619ad8410 Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Tue, 10 Dec 2024 15:22:43 +0200 Subject: [PATCH 11/15] build: install flow as NPM package --- bun.lockb | Bin 43014 -> 42986 bytes package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/bun.lockb b/bun.lockb index 20396e907af08d288f5a4dc1031e3a791e23d2f6..aa37a6f16e83688203013aee72efebb88128b735 100755 GIT binary patch delta 5500 zcmeHLeQ;FO6@Pb=P4?x3kPk^;R*VURBn1{Wo3L4yC5DNQY))eaU%gj$qnai&y5oQkpvLx1OeNZLXF=)d*O zp7(p_+;h)8_uPBWeS3S}v|Ku6`H3U#iv@vKKfAK~>NKzCetE;LTYEZk~;E#pr@^!!% z)Xxc*q-0>8Ere!T#-icP+VG$HK68oUV4rPG113w4P9e!ls>p=Yc zz381g*aF1h`~liw$yBDnN47Q@$kG!)j;o!{Su%ZVjHoUHveqd~AZr~5a-TgIF%P01 zI1*UX)U={$e9iieimpG;s82}IeQE@zL(c}}_Dg_l?}56ihUFOBK@5NgUsu&UerZEh zb?q()c#z+;ID&A52i1$-88-uYu<3W`hO3MLd_xDdmDC@W9B(eFtErL%kNgt@a`@#i z(MF+2)p`PcsdPQkE3DLFQ{~A*l70XsD~Vxlxf7fRoS=?qw>Ux`S(U$pY?7wYVRMV$ zP!O^~I5Gt?i3DWh$P=ZCE^3KV<=3zexW-ByQEv1Rf~*#Mq!O|)&FWEViB{#WAY;97 zmdUfRONzi*G>xs~iBaWqkWJNPu#`c;7*$kJ2+zIbiB&}mwcuGqK|I$}C{~qEL`u>e zw9vgDK`p~nQBT2Ps&$u5k|tAIv{$@Co;X#G#WdXo%}A}kg%pfatuI4XL~T)C%QcxM z4==qlylc>sbuuf)Zb`UmsFF2kQgah1VaNI|P7OGiT&+K{_ zp|$)dxVtsYz8JSSN5LdjuEKudU~1u(p8}`rbD%GRa+a_g%s8LHe}POEoO>sgg!E%E$c>bx>1qr7SB>BMU~G(#-=q55l=04RW8Q<<3R{2 zMl?542(k|hnZ8hOM+(rFHx|D2F>rTNL4w!XkGE<%8tWC)$TL!vdyq2NIf;iZ{z$=* zsx=*(aVi}h>9szJx260hU%?yO!ypiCC-MXLulKbaTn;!3iCDKdMxInv4!d0+iN2M* z6ohOGWcrM8U$2qpPF0RZ<}mDHTh9Qe2OHy(_ZXbs%LQ<}BH+Po`L;WB2lT~N1I~-8 zC~YtAp-`GCpN5Q=OeF7oc?41n2Uas>n+=W~q2h%TX)w|zFe&LOdo)Uw6YkVkl0+St z%4%@;L)F4lDSk(xQK}N2rl-+#NzZm`osN8m6DgF+M$htDAjM+W3L{n%Cts}$MPv;r z6NIcKWoflKA}>i}4Sp!H++oNWx!nWuG{rnkDoOtfaz~RuY!8#J z*+aB7UIlWwi3|Q)pf@Ojm`5-T#1Se1abe^RrZa;w6p^x|G7#4z4@nZWWhZCzOF0NR zRGJUsTBFndM0z1Rlg5o14zi_jpNl|TZiRx{#w7cAPL^moI4_~w`(@%4li1W)&RLd3y90DkVD!EVgqd;E<=&!?I1>dW$4I` zcYwGsa($;Xmsx(%3nMFT0vw?P9>dvLi!-~+~OCOegAw~VfTc=DfgDYYJETUQ01B2#jiYjc}D%mhsW-@ z*f*hM=X;^Ia|RZSYJRSARrc`@*E&B;9Vr4Yj!zgP2C1(+(Y6me7RQc1@M%S>z~L57 z_gC0#_uKRjbm}i(5_q%Xki|A1Uo69Pz0H=uV0p0!%v^Xri3ZEB(3qx&MJgR^N-Rh> zmgjOHrvOf9t3g~EKrD*@-3#K<2x1v0=QSjn6SFyMF}D)LH8N-;h-IrlEW;P8)J6r( z3q%}kZ=R9C38n|M8T1f{6Hyn4Q!0DHJufD)I?>0eoX@mO&?wMoPzH$4Jw7Bkb#dPF zgE#@N0I?_BGbijNAU@mNAbcP2XN)usG#-=<;sdq=#5tMA%}KikG#xYp2NJE$@bQ-B zb8#(bJqVu*(n1g)QWc;|5NFnTAU@8DK*gY`AU;R&`6Cs83PBq{e3+Ghrh#HW95J2; zCy394`#>C3jv{h>f_4CKl3&TSD7{p=W^UjYYpx3M5c6Uk&r<&+8RYN51u?g5OIiw)uUB+`ajoMf$7SXDWifE)Q8x^~G zLvg)999Wrt*I!{H*OB9J;VuI$6uWtoF<(6K?({EquS9(w`al@8dQn2Lt%}{e-*|q7 z^H9UU15ZNBrF-J0;#NgWr3K%r^#(oKs^pos9)Fql%Qt6!w)>(*v^yP62bPg}W}eE7&!)KAi9$GoVCQ$BIzJw7)dHOA~dMkD=--Mp|la&3RSn)^(x*1r~o z3@U?{+F0Q*Er@GRmEl)}e?kDmpss z=3Pyy<%30K0|P_W<23pdS|=#FQxSH`=&Th%`f;aA9H%}=PSLxah$sG@AyxaNw!Jl;|MLjnP z^&DvI1{-bbQbYpn?{bMH^giA-iQT;Q`Do6Du+oYhhtb0A$jNoMB{z-Vq}a_%o~=*4 zRB^rSR0Zle8z7w?q^eCWyLls6y=rtpQFI*7kP{BWBRlV7rc|{O;2o4}KAgCcL zRkE7~o2CiTrg>?SMN$`SX(hBP*2EuPVF`{yUeS~3s+ zb}Zr3=Dy_~IUq^W4U!bt+1HmSw)Q=iTzbSJN#T$*m6p|%LwnfZM?-XZ8!!_3HKCG} z2)qRt0jxLli>h0}ya0 zuQxe{5eRpxAKn@70&-_lr|J!t8y&cggUSu`dPridIjKHNmLxzl`8)zS5)hzg!X{HzvP>RXm9IiL z4?-(-+T7yjGz8fvh;9L75(&t%$P=!L{p1T*oxKrBeXT!koB=JdvvP5@d@gf64+Hid0281@V4@JW;BMAs^mFG!&)E z?O~F%OshF!bIYf}tpF#qcBYVTqAF@>Xrd|)*d%E_G@5sDfjrTw9E%Ywf-Fq4E~TMp z)p`=L0@@SqwMgMKKW3r46vM4MK!>dT;PNOx#w*4s7^8}-~-D zB+4yM#JbJ`hq%$(Rp2;CeSi;9P*LSqAzS=K?TiGyYw(Q;Q4daUStPo}V-%dC%CAGl z?Q0g|2J+ce`6et!?giRK7X1{2>}^A)Ptj@E5%h_TDQ~?W+)~Pq^IG3Rsf>oBykZG? zl2o}D3y33>xbNZ=4JE18RIJTKG@RtM5=s?Zl7lF5dqPXM3)=L05#<)6 z_(Y<%XP*?J5kcH00xvdsS`78QK00 zgJI|Nd)7fZ_7Yf?L$Q7G(Rzq&M8N?M@4f(Z@)4K=6^*ao^@yK$X9gG1GKLHMj z9dv1|j%={k;Kw7|^?+Ev&(L2BInZ81|6fRAj`Qm^qGhMR$Ppvwv=GFDSOnr+^V;Bt zk@e%(2FscMdX0PqYXqz2D_tWzxY~0ynm=Tv!N+bK6|`V=p|A`d&xlJEpU}wac-t`q z6^%7K*tfo2&|kwsY2#X(Z3{jc`Jn{5d=5Rd zeps&B)wtcq-f$9T4nDO0*^ukJvMrR_&?~0VGY#?iGf;u&D*un*5P5go1>&a$#IgyX z6(D|UK`aXcHIrzJPvb3&xt$=^$e?x*%j!Wa3kP*je&cQQgT^9%3OL?x_Ja<9?ga7v z)D5}=#D3Y&7Emdu473%r4YV1=haaD~GeNUJsUSWeGeNws1wfr3-kmurjskynwOzfO zKW_LG#3zL00Xab#AO~nJh>zkf5bxZa2;RvnsITcp|4Nh!Kzw$#fH>KFRBZzBf%YxX zI?#I1w?X*G*0zF0z;A;1tj!1Eb45}?Z6JJ-Xxl;&FcQScn*qWHn08jI26BRt&;;!; z;DdoT|DDDl^;5~Nbz;Kcz^;D@@e)1IGMi4dblEvwJb7OH*#nk}qu$*X8=vR+S`6%@ zGrJWLAphc!G+#;8j`)-npE&8O~Ko9tw_y=^(r-&W&0p32c zwpNNRs%>?OLo|T*Vfr23M@Vifv6qGD^^W{x?*;qhcW$wWO}UO-2Rc+w^=*o1qr2J^ zyLsJ_H2w7%C*|owu$t${;*YOU`emEqy1w4L2>Ec^hP9U#oGe1kJk-Rnt)ZxP#cuxR zU6%R$!^-K=Nt%C6znBW!6`@<1*Cl23GxG}~qVr&tqx(;$gYAmTyeLsl^~sldo_+#) zClZ9oV*cxW^+4*?g|{7$p~=?$U!hCwC1NezxW_35X!9OLJWIYkB_f?(*i&LRuTGX9 zJJ)jg_lnCRIt-7!l-Z%oGjBrXP5MKNXXTHlYVD$BNiy$DCT5>|bLdyOn+%W0RY)Xt zbW9d2=)Ml8c!OT-P{c7>8BoLt`n*GNnU^EZf4sl(De-8S)(I^Y=IzF<>8XQ9Qd4rE z$wBwBv!shu;#chEElB13h09)P{Oo4vT@HjRNuN@uUlHMSKdj8lm@RSpXC1$%wcBU~ zL!C}%{ff)H%!yVmJ6!$iILR(O$>#M=R><8=eKn=`X*IRsFQlY^VmEJP_O6hp@BU#6 zr#}}p(X!sZBHA8sib8rYpt#H{pN~SNCtvtz-Er6&6XN>)*L!K+3Du@eEqnaCWy>wX zsfQ;Wq?Aqt|EIm3onj#kcPe)C#>e{3z4LbTACH6=W7tpA$FNPNh^|U8OpRSmyLr!3 z<=Xs6X8PrOVQXaXWg6*H#6`M*_eT1Am(yk50eO2LzL=8|9D!|)17YCH3#&C23%2z_ za?DFxGkluIYzHmMJx<&y{BvItBW zUK7$?^i;RvGOv?9{Abyv&GEL}FQYIomOhA_rbKkj;`zp@MtoknqIpT|af;WdxJR*@ z*Gb*#EB2*hrnW#?@3aqI-Tz_ozX3Km B+|~d9 diff --git a/package.json b/package.json index 0358315..83d526c 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dependencies": { "@openzeppelin/contracts": "5.0.2", "@prb/math": "4.1.0", - "@sablier/flow": "github:sablier-labs/flow", + "@sablier/flow": "^1.0.0", "@sablier/v2-core": "1.2.0", "@sablier/v2-periphery": "1.2.0" }, From 164a940c5e32c690fbc8cdb2dd6d1f32728acbe2 Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Tue, 10 Dec 2024 15:25:25 +0200 Subject: [PATCH 12/15] refactor: rename contract to FlowStreamManager --- flow/{FlowManager.sol => FlowStreamManager.sol} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename flow/{FlowManager.sol => FlowStreamManager.sol} (98%) diff --git a/flow/FlowManager.sol b/flow/FlowStreamManager.sol similarity index 98% rename from flow/FlowManager.sol rename to flow/FlowStreamManager.sol index 11d6af4..6c246c9 100644 --- a/flow/FlowManager.sol +++ b/flow/FlowStreamManager.sol @@ -5,7 +5,7 @@ import { ud21x18 } from "@prb/math/src/UD21x18.sol"; import { ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol"; -contract FlowManager { +contract FlowStreamManager { ISablierFlow public immutable sablierFlow; constructor(ISablierFlow sablierFlow_) { From aa17487f5aa6f13829f6e1f669cbda99f5f9c765 Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Tue, 10 Dec 2024 16:40:56 +0200 Subject: [PATCH 13/15] refactor: declare the flow contract as constant polish code use the latest deployment --- flow/FlowBatchable.sol | 72 +++++++++++++++++------------------- flow/FlowBatchable.t.sol | 14 ++----- flow/FlowStreamCreator.sol | 13 ++----- flow/FlowStreamCreator.t.sol | 18 +++------ flow/FlowStreamManager.sol | 29 +++++++-------- 5 files changed, 60 insertions(+), 86 deletions(-) diff --git a/flow/FlowBatchable.sol b/flow/FlowBatchable.sol index c1634c9..c2745e5 100644 --- a/flow/FlowBatchable.sol +++ b/flow/FlowBatchable.sol @@ -4,19 +4,15 @@ 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, SablierFlow } from "@sablier/flow/src/SablierFlow.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); - SablierFlow public immutable sablierFlow; - - constructor(SablierFlow sablierFlow_) { - sablierFlow = sablierFlow_; - } + 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 { @@ -27,17 +23,17 @@ contract FlowBatchable { USDC.transferFrom(msg.sender, address(this), depositAmount); // Approve the Sablier contract to spend USDC. - USDC.approve(address(sablierFlow), depositAmount); + USDC.approve(address(FLOW), depositAmount); // Fetch the stream recipient. - address recipient = sablierFlow.getRecipient(streamId); + address recipient = FLOW.getRecipient(streamId); // The call data declared as bytes. bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeCall(sablierFlow.adjustRatePerSecond, (streamId, newRatePerSecond)); - calls[1] = abi.encodeCall(sablierFlow.deposit, (streamId, depositAmount, msg.sender, recipient)); + calls[0] = abi.encodeCall(FLOW.adjustRatePerSecond, (streamId, newRatePerSecond)); + calls[1] = abi.encodeCall(FLOW.deposit, (streamId, depositAmount, msg.sender, recipient)); - sablierFlow.batch(calls); + FLOW.batch(calls); } /// @dev A function to create a stream and deposit via a broker in a single transaction. @@ -57,17 +53,17 @@ contract FlowBatchable { USDC.transferFrom(msg.sender, address(this), depositAmount); // Approve the Sablier contract to spend USDC. - USDC.approve(address(sablierFlow), depositAmount); + USDC.approve(address(FLOW), depositAmount); - streamId = sablierFlow.nextStreamId(); + streamId = FLOW.nextStreamId(); // The call data declared as bytes bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeCall(sablierFlow.create, (sender, recipient, ratePerSecond, USDC, true)); - calls[1] = abi.encodeCall(sablierFlow.depositViaBroker, (streamId, depositAmount, sender, recipient, broker)); + 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. - sablierFlow.batch(calls); + FLOW.batch(calls); } /// @dev A function to create multiple streams in a single transaction. @@ -80,17 +76,17 @@ contract FlowBatchable { // The call data declared as bytes bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeCall(sablierFlow.create, (sender, firstRecipient, firstRatePerSecond, USDC, true)); - calls[1] = abi.encodeCall(sablierFlow.create, (sender, secondRecipient, secondRatePerSecond, USDC, true)); + 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 = sablierFlow.nextStreamId(); + 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. - sablierFlow.batch(calls); + FLOW.batch(calls); } /// @dev A function to create multiple streams and deposit via a broker into all the stream in a single transaction. @@ -105,7 +101,7 @@ contract FlowBatchable { USDC.transferFrom(msg.sender, address(this), 2 * depositAmount); // Approve the Sablier contract to spend USDC. - USDC.approve(address(sablierFlow), 2 * depositAmount); + USDC.approve(address(FLOW), 2 * depositAmount); // The broker struct Broker memory broker = Broker({ @@ -113,44 +109,42 @@ contract FlowBatchable { fee: ud60x18(0.0001e18) // the fee percentage }); - uint256 nextStreamId = sablierFlow.nextStreamId(); + 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(sablierFlow.create, (sender, firstRecipient, ratePerSecond, USDC, true)); - calls[1] = abi.encodeCall(sablierFlow.create, (sender, secondRecipient, ratePerSecond, USDC, true)); - calls[2] = - abi.encodeCall(sablierFlow.depositViaBroker, (streamIds[0], depositAmount, sender, firstRecipient, broker)); - calls[3] = - abi.encodeCall(sablierFlow.depositViaBroker, (streamIds[1], depositAmount, sender, secondRecipient, broker)); + 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. - sablierFlow.batch(calls); + 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(sablierFlow.pause, (streamId)); - calls[1] = abi.encodeCall(sablierFlow.withdrawMax, (streamId, address(0xCAFE))); + 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. - sablierFlow.batch(calls); + 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(sablierFlow.void, (streamId)); - calls[1] = abi.encodeCall(sablierFlow.withdrawMax, (streamId, address(0xCAFE))); + 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. - sablierFlow.batch(calls); + FLOW.batch(calls); } /// @dev A function to withdraw maximum available funds from multiple streams in a single transaction. @@ -160,11 +154,11 @@ contract FlowBatchable { // 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 = sablierFlow.getRecipient(streamIds[i]); - calls[i] = abi.encodeCall(sablierFlow.withdrawMax, (streamIds[i], recipient)); + 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. - sablierFlow.batch(calls); + FLOW.batch(calls); } } diff --git a/flow/FlowBatchable.t.sol b/flow/FlowBatchable.t.sol index 0d38352..1dd9a87 100644 --- a/flow/FlowBatchable.t.sol +++ b/flow/FlowBatchable.t.sol @@ -5,22 +5,16 @@ import { Test } from "forge-std/src/Test.sol"; import { FlowBatchable } from "./FlowBatchable.sol"; -import { IFlowNFTDescriptor, SablierFlow } from "@sablier/flow/src/SablierFlow.sol"; - contract FlowBatchable_Test is Test { FlowBatchable internal batchable; - SablierFlow internal flow; address internal user; function setUp() external { // Fork Ethereum Sepolia - vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 6_240_816 }); - - // Deploy a SablierFlow contract - flow = new SablierFlow({ initialAdmin: address(this), initialNFTDescriptor: IFlowNFTDescriptor(address(this)) }); + vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_250_564 }); // Deploy the batchable contract - batchable = new FlowBatchable(flow); + batchable = new FlowBatchable(); user = makeAddr("User"); @@ -35,7 +29,7 @@ contract FlowBatchable_Test is Test { } function test_CreateMultiple() external { - uint256 nextStreamIdBefore = flow.nextStreamId(); + uint256 nextStreamIdBefore = batchable.FLOW().nextStreamId(); uint256[] memory actualStreamIds = batchable.createMultiple(); uint256[] memory expectedStreamIds = new uint256[](2); @@ -46,7 +40,7 @@ contract FlowBatchable_Test is Test { } function test_CreateAndDepositViaBroker() external { - uint256 nextStreamIdBefore = flow.nextStreamId(); + uint256 nextStreamIdBefore = batchable.FLOW().nextStreamId(); uint256[] memory actualStreamIds = batchable.createMultipleAndDepositViaBroker(); uint256[] memory expectedStreamIds = new uint256[](2); diff --git a/flow/FlowStreamCreator.sol b/flow/FlowStreamCreator.sol index 977f411..5aed573 100644 --- a/flow/FlowStreamCreator.sol +++ b/flow/FlowStreamCreator.sol @@ -3,26 +3,21 @@ 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 { - // Mainnet USDC + // Sepolia addresses IERC20 public constant USDC = IERC20(0xf08A50178dfcDe18524640EA6618a1f965821715); - ISablierFlow public immutable sablierFlow; - - constructor(ISablierFlow sablierFlow_) { - sablierFlow = sablierFlow_; - } + 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 = sablierFlow.create({ + 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 @@ -36,7 +31,7 @@ contract FlowStreamCreator { UD21x18 ratePerSecond = FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: 1_000_000e6, duration: 365 days }); - streamId = sablierFlow.create({ + 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 diff --git a/flow/FlowStreamCreator.t.sol b/flow/FlowStreamCreator.t.sol index 2d11669..7bbb475 100644 --- a/flow/FlowStreamCreator.t.sol +++ b/flow/FlowStreamCreator.t.sol @@ -5,22 +5,16 @@ import { Test } from "forge-std/src/Test.sol"; import { FlowStreamCreator } from "./FlowStreamCreator.sol"; -import { IFlowNFTDescriptor, SablierFlow } from "@sablier/flow/src/SablierFlow.sol"; - contract FlowStreamCreator_Test is Test { FlowStreamCreator internal streamCreator; - SablierFlow internal flow; address internal user; function setUp() external { // Fork Ethereum Sepolia - vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 6_240_816 }); - - // Deploy a SablierFlow contract - flow = new SablierFlow({ initialAdmin: address(this), initialNFTDescriptor: IFlowNFTDescriptor(address(this)) }); + vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_250_564 }); // Deploy the FlowStreamCreator contract - streamCreator = new FlowStreamCreator(flow); + streamCreator = new FlowStreamCreator(); user = makeAddr("User"); @@ -35,7 +29,7 @@ contract FlowStreamCreator_Test is Test { } function test_CreateStream_1K_PerMonth() external { - uint256 expectedStreamId = flow.nextStreamId(); + uint256 expectedStreamId = streamCreator.FLOW().nextStreamId(); uint256 actualStreamId = streamCreator.createStream_1K_PerMonth(); assertEq(actualStreamId, expectedStreamId); @@ -43,11 +37,11 @@ contract FlowStreamCreator_Test is Test { // 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(flow.totalDebtOf(actualStreamId), 1000e6); + assertGe(streamCreator.FLOW().totalDebtOf(actualStreamId), 1000e6); } function test_CreateStream_1M_PerYear() external { - uint256 expectedStreamId = flow.nextStreamId(); + uint256 expectedStreamId = streamCreator.FLOW().nextStreamId(); uint256 actualStreamId = streamCreator.createStream_1M_PerYear(); assertEq(actualStreamId, expectedStreamId); @@ -55,6 +49,6 @@ contract FlowStreamCreator_Test is Test { // 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(flow.totalDebtOf(actualStreamId), 1_000_000e6); + assertGe(streamCreator.FLOW().totalDebtOf(actualStreamId), 1_000_000e6); } } diff --git a/flow/FlowStreamManager.sol b/flow/FlowStreamManager.sol index 6c246c9..7c63687 100644 --- a/flow/FlowStreamManager.sol +++ b/flow/FlowStreamManager.sol @@ -6,53 +6,50 @@ import { ud21x18 } from "@prb/math/src/UD21x18.sol"; import { ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol"; contract FlowStreamManager { - ISablierFlow public immutable sablierFlow; - - constructor(ISablierFlow sablierFlow_) { - sablierFlow = sablierFlow_; - } + // Sepolia addres + ISablierFlow public constant FLOW = ISablierFlow(0x5Ae8c13f6Ae094887322012425b34b0919097d8A); function adjustRatePerSecond(uint256 streamId) external { - sablierFlow.adjustRatePerSecond({ streamId: streamId, newRatePerSecond: ud21x18(0.0001e18) }); + FLOW.adjustRatePerSecond({ streamId: streamId, newRatePerSecond: ud21x18(0.0001e18) }); } function deposit(uint256 streamId) external { - sablierFlow.deposit(streamId, 3.14159e18, msg.sender, address(0xCAFE)); + FLOW.deposit(streamId, 3.14159e18, msg.sender, address(0xCAFE)); } function depositAndPause(uint256 streamId) external { - sablierFlow.depositAndPause(streamId, 3.14159e18); + FLOW.depositAndPause(streamId, 3.14159e18); } function pause(uint256 streamId) external { - sablierFlow.pause(streamId); + FLOW.pause(streamId); } function refund(uint256 streamId) external { - sablierFlow.refund({ streamId: streamId, amount: 1.61803e18 }); + FLOW.refund({ streamId: streamId, amount: 1.61803e18 }); } function refundAndPause(uint256 streamId) external { - sablierFlow.refundAndPause({ streamId: streamId, amount: 1.61803e18 }); + FLOW.refundAndPause({ streamId: streamId, amount: 1.61803e18 }); } function restart(uint256 streamId) external { - sablierFlow.restart({ streamId: streamId, ratePerSecond: ud21x18(0.0001e18) }); + FLOW.restart({ streamId: streamId, ratePerSecond: ud21x18(0.0001e18) }); } function restartAndDeposit(uint256 streamId) external { - sablierFlow.restartAndDeposit({ streamId: streamId, ratePerSecond: ud21x18(0.0001e18), amount: 2.71828e18 }); + FLOW.restartAndDeposit({ streamId: streamId, ratePerSecond: ud21x18(0.0001e18), amount: 2.71828e18 }); } function void(uint256 streamId) external { - sablierFlow.void(streamId); + FLOW.void(streamId); } function withdraw(uint256 streamId) external { - sablierFlow.withdraw({ streamId: streamId, to: address(0xCAFE), amount: 2.71828e18 }); + FLOW.withdraw({ streamId: streamId, to: address(0xCAFE), amount: 2.71828e18 }); } function withdrawMax(uint256 streamId) external { - sablierFlow.withdrawMax({ streamId: streamId, to: address(0xCAFE) }); + FLOW.withdrawMax({ streamId: streamId, to: address(0xCAFE) }); } } From 982dcab9a6d3def520fef6278c3f396ee656ac7f Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Wed, 11 Dec 2024 15:31:06 +0200 Subject: [PATCH 14/15] refactor: calculate first the duration in ratePerSecondForTimestamps --- flow/FlowUtilities.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flow/FlowUtilities.sol b/flow/FlowUtilities.sol index caf1cde..10369db 100644 --- a/flow/FlowUtilities.sol +++ b/flow/FlowUtilities.sol @@ -54,12 +54,12 @@ library FlowUtilities { view returns (UD21x18 ratePerSecond) { - // Get the decimals of the token. - uint8 decimals = IERC20Metadata(token).decimals(); - // Calculate the duration. uint40 duration = end - start; + // Get the decimals of the token. + uint8 decimals = IERC20Metadata(token).decimals(); + if (decimals == 18) { return ud21x18(amount / duration); } From bf31f572a9580134dc7a66a32533cabbf422cada Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Thu, 12 Dec 2024 15:13:04 +0200 Subject: [PATCH 15/15] feat: add all functions in FlowStreamManager --- flow/FlowStreamManager.sol | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/flow/FlowStreamManager.sol b/flow/FlowStreamManager.sol index 7c63687..9cbc598 100644 --- a/flow/FlowStreamManager.sol +++ b/flow/FlowStreamManager.sol @@ -2,11 +2,11 @@ pragma solidity >=0.8.22; import { ud21x18 } from "@prb/math/src/UD21x18.sol"; - -import { ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol"; +import { ud60x18 } from "@prb/math/src/UD60x18.sol"; +import { Broker, ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol"; contract FlowStreamManager { - // Sepolia addres + // Sepolia address ISablierFlow public constant FLOW = ISablierFlow(0x5Ae8c13f6Ae094887322012425b34b0919097d8A); function adjustRatePerSecond(uint256 streamId) external { @@ -21,6 +21,18 @@ contract FlowStreamManager { FLOW.depositAndPause(streamId, 3.14159e18); } + function depositViaBroker(uint256 streamId) external { + Broker memory broker = Broker({ account: address(0xDEAD), fee: ud60x18(0.0001e18) }); + + FLOW.depositViaBroker({ + streamId: streamId, + totalAmount: 3.14159e18, + sender: msg.sender, + recipient: address(0xCAFE), + broker: broker + }); + } + function pause(uint256 streamId) external { FLOW.pause(streamId); } @@ -33,6 +45,10 @@ contract FlowStreamManager { FLOW.refundAndPause({ streamId: streamId, amount: 1.61803e18 }); } + function refundMax(uint256 streamId) external { + FLOW.refundMax(streamId); + } + function restart(uint256 streamId) external { FLOW.restart({ streamId: streamId, ratePerSecond: ud21x18(0.0001e18) }); }