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/.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 d3bf753..aa37a6f 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/flow/FlowBatchable.sol b/flow/FlowBatchable.sol new file mode 100644 index 0000000..c2745e5 --- /dev/null +++ b/flow/FlowBatchable.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ud21x18, UD21x18 } from "@prb/math/src/UD21x18.sol"; +import { ud60x18 } from "@prb/math/src/UD60x18.sol"; +import { Broker, ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol"; + +/// @notice The `Batch` contract, inherited in SablierFlow, allows multiple function calls to be batched together. This +/// enables any possible combination of functions to be executed within a single transaction. +/// @dev For some functions to work, `msg.sender` must have approved this contract to spend USDC. +contract FlowBatchable { + // Sepolia addresses + IERC20 public constant USDC = IERC20(0xf08A50178dfcDe18524640EA6618a1f965821715); + ISablierFlow public constant FLOW = ISablierFlow(0x5Ae8c13f6Ae094887322012425b34b0919097d8A); + + /// @dev A function to adjust the rate per second and deposit into a stream in a single transaction. + function adjustRatePerSecondAndDeposit(uint256 streamId) external { + UD21x18 newRatePerSecond = ud21x18(0.0001e18); + uint128 depositAmount = 1000e6; + + // Transfer to this contract the amount to deposit in the stream. + USDC.transferFrom(msg.sender, address(this), depositAmount); + + // Approve the Sablier contract to spend USDC. + USDC.approve(address(FLOW), depositAmount); + + // Fetch the stream recipient. + address recipient = FLOW.getRecipient(streamId); + + // The call data declared as bytes. + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(FLOW.adjustRatePerSecond, (streamId, newRatePerSecond)); + calls[1] = abi.encodeCall(FLOW.deposit, (streamId, depositAmount, msg.sender, recipient)); + + FLOW.batch(calls); + } + + /// @dev A function to create a stream and deposit via a broker in a single transaction. + function createAndDepositViaBroker() external returns (uint256 streamId) { + address sender = msg.sender; + address recipient = address(0xCAFE); + UD21x18 ratePerSecond = ud21x18(0.0001e18); + uint128 depositAmount = 1000e6; + + // The broker struct. + Broker memory broker = Broker({ + account: address(0xDEAD), + fee: ud60x18(0.0001e18) // the fee percentage + }); + + // Transfer to this contract the amount to deposit in the stream. + USDC.transferFrom(msg.sender, address(this), depositAmount); + + // Approve the Sablier contract to spend USDC. + USDC.approve(address(FLOW), depositAmount); + + streamId = FLOW.nextStreamId(); + + // The call data declared as bytes + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(FLOW.create, (sender, recipient, ratePerSecond, USDC, true)); + calls[1] = abi.encodeCall(FLOW.depositViaBroker, (streamId, depositAmount, sender, recipient, broker)); + + // Execute multiple calls in a single transaction using the prepared call data. + FLOW.batch(calls); + } + + /// @dev A function to create multiple streams in a single transaction. + function createMultiple() external returns (uint256[] memory streamIds) { + address sender = msg.sender; + address firstRecipient = address(0xCAFE); + address secondRecipient = address(0xBEEF); + UD21x18 firstRatePerSecond = ud21x18(0.0001e18); + UD21x18 secondRatePerSecond = ud21x18(0.0002e18); + + // The call data declared as bytes + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(FLOW.create, (sender, firstRecipient, firstRatePerSecond, USDC, true)); + calls[1] = abi.encodeCall(FLOW.create, (sender, secondRecipient, secondRatePerSecond, USDC, true)); + + // Prepare the `streamIds` array to return them + uint256 nextStreamId = FLOW.nextStreamId(); + streamIds = new uint256[](2); + streamIds[0] = nextStreamId; + streamIds[1] = nextStreamId + 1; + + // Execute multiple calls in a single transaction using the prepared call data. + FLOW.batch(calls); + } + + /// @dev A function to create multiple streams and deposit via a broker into all the stream in a single transaction. + function createMultipleAndDepositViaBroker() external returns (uint256[] memory streamIds) { + address sender = msg.sender; + address firstRecipient = address(0xCAFE); + address secondRecipient = address(0xBEEF); + UD21x18 ratePerSecond = ud21x18(0.0001e18); + uint128 depositAmount = 1000e6; + + // Transfer the deposit amount of USDC tokens to this contract for both streams + USDC.transferFrom(msg.sender, address(this), 2 * depositAmount); + + // Approve the Sablier contract to spend USDC. + USDC.approve(address(FLOW), 2 * depositAmount); + + // The broker struct + Broker memory broker = Broker({ + account: address(0xDEAD), + fee: ud60x18(0.0001e18) // the fee percentage + }); + + uint256 nextStreamId = FLOW.nextStreamId(); + streamIds = new uint256[](2); + streamIds[0] = nextStreamId; + streamIds[1] = nextStreamId + 1; + + // We need to have 4 different function calls, 2 for creating streams and 2 for depositing via broker + bytes[] memory calls = new bytes[](4); + calls[0] = abi.encodeCall(FLOW.create, (sender, firstRecipient, ratePerSecond, USDC, true)); + calls[1] = abi.encodeCall(FLOW.create, (sender, secondRecipient, ratePerSecond, USDC, true)); + calls[2] = abi.encodeCall(FLOW.depositViaBroker, (streamIds[0], depositAmount, sender, firstRecipient, broker)); + calls[3] = abi.encodeCall(FLOW.depositViaBroker, (streamIds[1], depositAmount, sender, secondRecipient, broker)); + + // Execute multiple calls in a single transaction using the prepared call data. + FLOW.batch(calls); + } + + /// @dev A function to pause a stream and withdraw the maximum available funds. + function pauseAndWithdrawMax(uint256 streamId) external { + // The call data declared as bytes. + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(FLOW.pause, (streamId)); + calls[1] = abi.encodeCall(FLOW.withdrawMax, (streamId, address(0xCAFE))); + + // Execute multiple calls in a single transaction using the prepared call data. + FLOW.batch(calls); + } + + /// @dev A function to void a stream and withdraw what is left. + function voidAndWithdrawMax(uint256 streamId) external { + // The call data declared as bytes + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(FLOW.void, (streamId)); + calls[1] = abi.encodeCall(FLOW.withdrawMax, (streamId, address(0xCAFE))); + + // Execute multiple calls in a single transaction using the prepared call data. + FLOW.batch(calls); + } + + /// @dev A function to withdraw maximum available funds from multiple streams in a single transaction. + function withdrawMaxMultiple(uint256[] calldata streamIds) external { + uint256 count = streamIds.length; + + // Iterate over the streamIds and prepare the call data for each stream. + bytes[] memory calls = new bytes[](count); + for (uint256 i = 0; i < count; ++i) { + address recipient = FLOW.getRecipient(streamIds[i]); + calls[i] = abi.encodeCall(FLOW.withdrawMax, (streamIds[i], recipient)); + } + + // Execute multiple calls in a single transaction using the prepared call data. + FLOW.batch(calls); + } +} diff --git a/flow/FlowBatchable.t.sol b/flow/FlowBatchable.t.sol new file mode 100644 index 0000000..1dd9a87 --- /dev/null +++ b/flow/FlowBatchable.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { Test } from "forge-std/src/Test.sol"; + +import { FlowBatchable } from "./FlowBatchable.sol"; + +contract FlowBatchable_Test is Test { + FlowBatchable internal batchable; + address internal user; + + function setUp() external { + // Fork Ethereum Sepolia + vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_250_564 }); + + // Deploy the batchable contract + batchable = new FlowBatchable(); + + user = makeAddr("User"); + + // Mint some DAI tokens to the test user, which will be pulled by the creator contract + deal({ token: address(batchable.USDC()), to: user, give: 1_000_000e6 }); + + // Make the test user the `msg.sender` in all following calls + vm.startPrank({ msgSender: user }); + + // Approve the batchable contract to pull USDC tokens from the test user + batchable.USDC().approve({ spender: address(batchable), value: 1_000_000e6 }); + } + + function test_CreateMultiple() external { + uint256 nextStreamIdBefore = batchable.FLOW().nextStreamId(); + + uint256[] memory actualStreamIds = batchable.createMultiple(); + uint256[] memory expectedStreamIds = new uint256[](2); + expectedStreamIds[0] = nextStreamIdBefore; + expectedStreamIds[1] = nextStreamIdBefore + 1; + + assertEq(actualStreamIds, expectedStreamIds); + } + + function test_CreateAndDepositViaBroker() external { + uint256 nextStreamIdBefore = batchable.FLOW().nextStreamId(); + + uint256[] memory actualStreamIds = batchable.createMultipleAndDepositViaBroker(); + uint256[] memory expectedStreamIds = new uint256[](2); + expectedStreamIds[0] = nextStreamIdBefore; + expectedStreamIds[1] = nextStreamIdBefore + 1; + + assertEq(actualStreamIds, expectedStreamIds); + } +} diff --git a/flow/FlowStreamCreator.sol b/flow/FlowStreamCreator.sol new file mode 100644 index 0000000..5aed573 --- /dev/null +++ b/flow/FlowStreamCreator.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { UD21x18 } from "@prb/math/src/UD21x18.sol"; +import { ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol"; + +import { FlowUtilities } from "./FlowUtilities.sol"; + +contract FlowStreamCreator { + // Sepolia addresses + IERC20 public constant USDC = IERC20(0xf08A50178dfcDe18524640EA6618a1f965821715); + ISablierFlow public constant FLOW = ISablierFlow(0x5Ae8c13f6Ae094887322012425b34b0919097d8A); + + // Create a stream that sends 1000 USDC per month. + function createStream_1K_PerMonth() external returns (uint256 streamId) { + UD21x18 ratePerSecond = + FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: 1000e6, duration: 30 days }); + + streamId = FLOW.create({ + sender: msg.sender, // The sender will be able to manage the stream + recipient: address(0xCAFE), // The recipient of the streamed assets + ratePerSecond: ratePerSecond, // The rate per second equivalent to 1000 USDC per month + token: USDC, // The token to be streamed + transferable: true // Whether the stream will be transferable or not + }); + } + + // Create a stream that sends 1,000,000 USDC per year. + function createStream_1M_PerYear() external returns (uint256 streamId) { + UD21x18 ratePerSecond = + FlowUtilities.ratePerSecondWithDuration({ token: address(USDC), amount: 1_000_000e6, duration: 365 days }); + + streamId = FLOW.create({ + sender: msg.sender, // The sender will be able to manage the stream + recipient: address(0xCAFE), // The recipient of the streamed assets + ratePerSecond: ratePerSecond, // The rate per second equivalent to 1,000,00 USDC per year + token: USDC, // The token to be streamed + transferable: true // Whether the stream will be transferable or not + }); + } +} diff --git a/flow/FlowStreamCreator.t.sol b/flow/FlowStreamCreator.t.sol new file mode 100644 index 0000000..7bbb475 --- /dev/null +++ b/flow/FlowStreamCreator.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { Test } from "forge-std/src/Test.sol"; + +import { FlowStreamCreator } from "./FlowStreamCreator.sol"; + +contract FlowStreamCreator_Test is Test { + FlowStreamCreator internal streamCreator; + address internal user; + + function setUp() external { + // Fork Ethereum Sepolia + vm.createSelectFork({ urlOrAlias: "sepolia", blockNumber: 7_250_564 }); + + // Deploy the FlowStreamCreator contract + streamCreator = new FlowStreamCreator(); + + user = makeAddr("User"); + + // Mint some DAI tokens to the test user, which will be pulled by the creator contract + deal({ token: address(streamCreator.USDC()), to: user, give: 1_000_000e6 }); + + // Make the test user the `msg.sender` in all following calls + vm.startPrank({ msgSender: user }); + + // Approve the streamCreator contract to pull USDC tokens from the test user + streamCreator.USDC().approve({ spender: address(streamCreator), value: 1_000_000e6 }); + } + + function test_CreateStream_1K_PerMonth() external { + uint256 expectedStreamId = streamCreator.FLOW().nextStreamId(); + + uint256 actualStreamId = streamCreator.createStream_1K_PerMonth(); + assertEq(actualStreamId, expectedStreamId); + + // Warp slightly over 30 days so that the debt accumulated is slightly over 1000 USDC. + vm.warp({ newTimestamp: block.timestamp + 30 days + 1 seconds }); + + assertGe(streamCreator.FLOW().totalDebtOf(actualStreamId), 1000e6); + } + + function test_CreateStream_1M_PerYear() external { + uint256 expectedStreamId = streamCreator.FLOW().nextStreamId(); + + uint256 actualStreamId = streamCreator.createStream_1M_PerYear(); + assertEq(actualStreamId, expectedStreamId); + + // Warp slightly over 365 days so that the debt accumulated is slightly over 1M USDC. + vm.warp({ newTimestamp: block.timestamp + 365 days + 1 seconds }); + + assertGe(streamCreator.FLOW().totalDebtOf(actualStreamId), 1_000_000e6); + } +} diff --git a/flow/FlowStreamManager.sol b/flow/FlowStreamManager.sol new file mode 100644 index 0000000..9cbc598 --- /dev/null +++ b/flow/FlowStreamManager.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { ud21x18 } from "@prb/math/src/UD21x18.sol"; +import { ud60x18 } from "@prb/math/src/UD60x18.sol"; +import { Broker, ISablierFlow } from "@sablier/flow/src/interfaces/ISablierFlow.sol"; + +contract FlowStreamManager { + // Sepolia address + ISablierFlow public constant FLOW = ISablierFlow(0x5Ae8c13f6Ae094887322012425b34b0919097d8A); + + function adjustRatePerSecond(uint256 streamId) external { + FLOW.adjustRatePerSecond({ streamId: streamId, newRatePerSecond: ud21x18(0.0001e18) }); + } + + function deposit(uint256 streamId) external { + FLOW.deposit(streamId, 3.14159e18, msg.sender, address(0xCAFE)); + } + + function depositAndPause(uint256 streamId) external { + 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); + } + + function refund(uint256 streamId) external { + FLOW.refund({ streamId: streamId, amount: 1.61803e18 }); + } + + function refundAndPause(uint256 streamId) external { + 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) }); + } + + function restartAndDeposit(uint256 streamId) external { + FLOW.restartAndDeposit({ streamId: streamId, ratePerSecond: ud21x18(0.0001e18), amount: 2.71828e18 }); + } + + function void(uint256 streamId) external { + FLOW.void(streamId); + } + + function withdraw(uint256 streamId) external { + FLOW.withdraw({ streamId: streamId, to: address(0xCAFE), amount: 2.71828e18 }); + } + + function withdrawMax(uint256 streamId) external { + FLOW.withdrawMax({ streamId: streamId, to: address(0xCAFE) }); + } +} diff --git a/flow/FlowUtilities.sol b/flow/FlowUtilities.sol new file mode 100644 index 0000000..10369db --- /dev/null +++ b/flow/FlowUtilities.sol @@ -0,0 +1,96 @@ +// 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 rate per second and streamed amount based on a given time frame. +library FlowUtilities { + /// @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 user wishes to stream. + /// @return ratePerSecond The rate per second as a 18-decimal 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 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 18-decimal fixed-point number. + function ratePerSecondForTimestamps( + address token, + uint128 amount, + uint40 start, + uint40 end + ) + internal + view + returns (UD21x18 ratePerSecond) + { + // 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); + } + + // 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 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 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 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) { + amountPerYear = 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 97% rename from v2/core/RecipientHooks.sol rename to lockup/core/RecipientHooks.sol index 52f038d..bbfdd02 100644 --- a/v2/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/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 91% rename from v2/core/StreamManagementWithHook.t.sol rename to lockup/core/StreamManagementWithHook.t.sol index c3a9bdc..25cf8c5 100644 --- a/v2/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/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..83d526c 100644 --- a/package.json +++ b/package.json @@ -12,11 +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.0.3", + "@prb/math": "4.1.0", + "@sablier/flow": "^1.0.0", "@sablier/v2-core": "1.2.0", "@sablier/v2-periphery": "1.2.0" }, @@ -40,12 +41,16 @@ "private": true, "repository": "github.com:sablier-labs/examples", "scripts": { - "build": "forge build", + "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", "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" + "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" } } 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/