diff --git a/.solhint.json b/.solhint.json index b7274a4e4..9fe91c18c 100644 --- a/.solhint.json +++ b/.solhint.json @@ -10,7 +10,7 @@ "func-visibility": ["error", { "ignoreConstructors": true }], "gas-custom-errors": "off", "max-states-count": ["warn", 20], - "max-line-length": ["error", 124], + "max-line-length": ["error", 130], "named-parameters-mapping": "warn", "no-empty-blocks": "off", "not-rely-on-time": "off", diff --git a/src/libraries/StreamingMath.sol b/src/libraries/StreamingMath.sol index 02f0207c9..df336e7ae 100644 --- a/src/libraries/StreamingMath.sol +++ b/src/libraries/StreamingMath.sol @@ -150,16 +150,16 @@ library StreamingMath { return 0; } - // If the end time is not in the future, return the deposited amount. - if (timestamps.end <= blockTimestamp) { - return depositedAmount; - } - // If the cliff time is in the future, return the start unlock amount. if (cliffTime > blockTimestamp) { return unlockAmounts.start; } + // If the end time is not in the future, return the deposited amount. + if (timestamps.end <= blockTimestamp) { + return depositedAmount; + } + unchecked { uint128 unlockAmountsSum = unlockAmounts.start + unlockAmounts.cliff; @@ -231,16 +231,16 @@ library StreamingMath { return 0; } - // If the end time is not in the future, return the deposited amount. - if (timestamps.end <= blockTimestamp) { - return depositedAmount; - } - // If the first tranche's timestamp is in the future, return zero. if (tranches[0].timestamp > blockTimestamp) { return 0; } + // If the end time is not in the future, return the deposited amount. + if (timestamps.end <= blockTimestamp) { + return depositedAmount; + } + // Sum the amounts in all tranches that have already been streamed. // Using unchecked arithmetic is safe because the sum of the tranche amounts is equal to the total amount // at this point. diff --git a/tests/fork/Fork.t.sol b/tests/fork/Fork.t.sol index b8d37a3de..8ece777b6 100644 --- a/tests/fork/Fork.t.sol +++ b/tests/fork/Fork.t.sol @@ -6,7 +6,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Base_Test } from "./../Base.t.sol"; -/// @notice Common logic needed by all fork tests. +/// @notice Base logic needed by the fork tests. abstract contract Fork_Test is Base_Test { /*////////////////////////////////////////////////////////////////////////// STATE VARIABLES @@ -14,7 +14,7 @@ abstract contract Fork_Test is Base_Test { IERC20 internal immutable FORK_TOKEN; address internal forkTokenHolder; - uint256 internal initialHolderBalance; + uint128 internal initialHolderBalance; /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -45,11 +45,14 @@ abstract contract Fork_Test is Base_Test { // Label the addresses. labelContracts(); - // Deal token balance to the user. - initialHolderBalance = 1e7 * 10 ** IERC20Metadata(address(FORK_TOKEN)).decimals(); + // Deal 1M tokens to the user. + initialHolderBalance = uint128(1e6 * (10 ** IERC20Metadata(address(FORK_TOKEN)).decimals())); deal({ token: address(FORK_TOKEN), to: forkTokenHolder, give: initialHolderBalance }); resetPrank({ msgSender: forkTokenHolder }); + + // Approve {SablierLockup} to transfer the holder's tokens. + approveContract({ token_: address(FORK_TOKEN), from: forkTokenHolder, spender: address(lockup) }); } /*////////////////////////////////////////////////////////////////////////// @@ -69,6 +72,9 @@ abstract contract Fork_Test is Base_Test { // Avoid users blacklisted by USDC or USDT. assumeNoBlacklisted(address(FORK_TOKEN), sender); assumeNoBlacklisted(address(FORK_TOKEN), recipient); + + // Make the holder the caller. + resetPrank(forkTokenHolder); } /// @dev Labels the most relevant addresses. diff --git a/tests/fork/Lockup.t.sol b/tests/fork/Lockup.t.sol new file mode 100644 index 000000000..b390699d9 --- /dev/null +++ b/tests/fork/Lockup.t.sol @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Solarray } from "solarray/src/Solarray.sol"; +import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; +import { Lockup, LockupLinear, LockupDynamic, LockupTranched } from "src/types/DataTypes.sol"; + +import { Fork_Test } from "./Fork.t.sol"; + +/// @notice Common Lockup logic needed by all the fork tests. +abstract contract Lockup_Fork_Test is Fork_Test { + /*////////////////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////////////////*/ + + // Struct with parameters to be fuzzed during the fork tests. + struct Params { + Lockup.CreateWithTimestamps create; + uint40 cliffTime; + LockupDynamic.Segment[] segments; + LockupTranched.Tranche[] tranches; + LockupLinear.UnlockAmounts unlockAmounts; + uint40 warpTimestamp; + uint128 withdrawAmount; + } + + // Struct to manage storage variables to be used across contracts. + struct Vars { + // Initial values + uint256 initialLockupBalance; + uint256 initialLockupBalanceETH; + uint256 initialRecipientBalance; + uint256 initialSenderBalance; + // Final values + uint256 actualHolderBalance; + uint256 actualLockupBalance; + uint256 actualRecipientBalance; + uint256 actualSenderBalance; + // Expected values + Lockup.Status expectedStatus; + // Generics + bool hasCliff; + bool isDepleted; + bool isSettled; + uint128 recipientAmount; + uint128 senderAmount; + uint128 streamedAmount; + uint256 streamId; + } + + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + Vars internal vars; + Lockup.Model internal lockupModel; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + constructor(IERC20 forkToken) Fork_Test(forkToken) { } + + /*////////////////////////////////////////////////////////////////////////// + CREATE HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev A pre-create helper function to set up the parameters for the stream creation. + function preCreateStream(Params memory params) internal { + checkUsers(params.create.sender, params.create.recipient, address(lockup)); + + // Store the pre-create token balances of Lockup and Holder. + uint256[] memory balances = + getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), forkTokenHolder)); + vars.initialLockupBalance = balances[0]; + initialHolderBalance = uint128(balances[1]); + + // Store the next stream ID. + vars.streamId = lockup.nextStreamId(); + + // Bound the start time. + params.create.timestamps.start = boundUint40( + params.create.timestamps.start, getBlockTimestamp() - 1000 seconds, getBlockTimestamp() + 10_000 seconds + ); + + vars.hasCliff = params.cliffTime > 0; + // Bound the cliff time. Since it is only relevant to the Linear model, it will be ignored for the Dynamic + // and Tranched models. + params.cliffTime = vars.hasCliff + ? boundUint40( + params.cliffTime, params.create.timestamps.start + 1 seconds, params.create.timestamps.start + 52 weeks + ) + : 0; + + // Set fixed values for shape name and token. + params.create.shape = "Custom shape"; + params.create.token = FORK_TOKEN; + + // Make the stream cancelable so that the cancel tests can be run. + params.create.cancelable = true; + + if (lockupModel == Lockup.Model.LOCKUP_LINEAR) { + // Bound the deposit amount. + params.create.depositAmount = boundUint128(params.create.depositAmount, 1, initialHolderBalance); + + // Bound the minimum value of end time so that it is always greater than the start time, and the cliff time. + uint40 endTimeLowerBound = maxOfTwo(params.create.timestamps.start, params.cliffTime); + + // Bound the end time of the stream. + params.create.timestamps.end = + boundUint40(params.create.timestamps.end, endTimeLowerBound + 1 seconds, MAX_UNIX_TIMESTAMP); + + // Bound the unlock amounts. + params.unlockAmounts.start = boundUint128(params.unlockAmounts.start, 0, params.create.depositAmount); + // Bound the cliff unlock amount only if the cliff is set. + params.unlockAmounts.cliff = vars.hasCliff + ? boundUint128(params.unlockAmounts.cliff, 0, params.create.depositAmount - params.unlockAmounts.start) + : 0; + } + + if (lockupModel == Lockup.Model.LOCKUP_DYNAMIC) { + fuzzSegmentTimestamps(params.segments, params.create.timestamps.start); + + // Fuzz the segment amounts and calculate the deposit. + params.create.depositAmount = + fuzzDynamicStreamAmounts({ upperBound: initialHolderBalance, segments: params.segments }); + + // Bound the end time of the stream. + params.create.timestamps.end = params.segments[params.segments.length - 1].timestamp; + } + + if (lockupModel == Lockup.Model.LOCKUP_TRANCHED) { + fuzzTrancheTimestamps(params.tranches, params.create.timestamps.start); + + // Fuzz the tranche amounts and calculate the deposit. + params.create.depositAmount = + fuzzTranchedStreamAmounts({ upperBound: initialHolderBalance, tranches: params.tranches }); + + // Bound the end time of the stream. + params.create.timestamps.end = params.tranches[params.tranches.length - 1].timestamp; + } + } + + /// @dev A post-create helper function to compare values and set up the parameters for withdraw and cancel tests. + function postCreateStream(Params memory params) internal { + // Check if the stream is settled. It is possible for a Lockup stream to settle at the time of creation in the + // following cases: + // 1. The streamed amount equals the deposited amount. + // 2. The end time is in the past. + vars.isSettled = vars.streamedAmount >= params.create.depositAmount + || lockup.getEndTime(vars.streamId) <= getBlockTimestamp(); + + // Check that the stream status is correct. + if (lockup.getStartTime(vars.streamId) > getBlockTimestamp()) { + vars.expectedStatus = Lockup.Status.PENDING; + } else if (vars.isSettled) { + vars.expectedStatus = Lockup.Status.SETTLED; + } else { + vars.expectedStatus = Lockup.Status.STREAMING; + } + assertEq(lockup.statusOf(vars.streamId), vars.expectedStatus, "post-create stream status"); + + if (vars.isSettled) { + // If the stream is settled, it should not be cancelable. + assertFalse(lockup.isCancelable(vars.streamId), "isCancelable"); + } else { + // Otherwise, it should match the parameter value. + assertEq(lockup.isCancelable(vars.streamId), params.create.cancelable, "isCancelable"); + } + + // Store the post-create token balances of Lockup and Holder. + uint256[] memory balances = + getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), forkTokenHolder)); + vars.actualLockupBalance = balances[0]; + vars.actualHolderBalance = balances[1]; + + // Assert that the Lockup contract's balance has been updated. + uint256 expectedLockupBalance = vars.initialLockupBalance + params.create.depositAmount; + assertEq(vars.actualLockupBalance, expectedLockupBalance, "post-create Lockup balance"); + + // Assert that the holder's balance has been updated. + uint128 expectedHolderBalance = initialHolderBalance - params.create.depositAmount; + assertEq(vars.actualHolderBalance, expectedHolderBalance, "post-create Holder balance"); + } + + /*////////////////////////////////////////////////////////////////////////// + WITHDRAW HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev A shared withdraw function to be used by all the fork tests. + function withdraw(Params memory params) internal { + // Bound the withdraw amount. + params.withdrawAmount = boundUint128(params.withdrawAmount, 0, lockup.withdrawableAmountOf(vars.streamId)); + + // Check if the stream has settled or will get depleted. It is possible for the stream to be just settled + // and not depleted because the withdraw amount is fuzzed. + vars.isSettled = vars.streamedAmount >= params.create.depositAmount + || lockup.getEndTime(vars.streamId) <= getBlockTimestamp(); + vars.isDepleted = params.withdrawAmount == params.create.depositAmount; + + // Run the withdraw tests only if the withdraw amount is not zero. + if (params.withdrawAmount > 0) { + // Load the pre-withdraw token balances. + vars.initialLockupBalance = vars.actualLockupBalance; + vars.initialLockupBalanceETH = address(lockup).balance; + vars.initialRecipientBalance = FORK_TOKEN.balanceOf(params.create.recipient); + + // Expect the relevant events to be emitted. + vm.expectEmit({ emitter: address(lockup) }); + emit ISablierLockupBase.WithdrawFromLockupStream({ + streamId: vars.streamId, + to: params.create.recipient, + token: FORK_TOKEN, + amount: params.withdrawAmount + }); + vm.expectEmit({ emitter: address(lockup) }); + emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); + + // Make the withdrawal and pay a fee. + resetPrank({ msgSender: params.create.recipient }); + vm.deal({ account: params.create.recipient, newBalance: 100 ether }); + lockup.withdraw{ value: FEE }({ + streamId: vars.streamId, + to: params.create.recipient, + amount: params.withdrawAmount + }); + + // Assert that the stream's status is correct. + if (vars.isDepleted) { + vars.expectedStatus = Lockup.Status.DEPLETED; + } else if (vars.isSettled) { + vars.expectedStatus = Lockup.Status.SETTLED; + } else { + vars.expectedStatus = Lockup.Status.STREAMING; + } + assertEq(lockup.statusOf(vars.streamId), vars.expectedStatus, "post-withdraw stream status"); + + // Assert that the withdrawn amount has been updated. + assertEq(lockup.getWithdrawnAmount(vars.streamId), params.withdrawAmount, "post-withdraw withdrawnAmount"); + + // Load the post-withdraw token balances. + uint256[] memory balances = + getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), params.create.recipient)); + vars.actualLockupBalance = balances[0]; + vars.actualRecipientBalance = balances[1]; + + // Assert that the contract's balance has been updated. + uint256 expectedLockupBalance = vars.initialLockupBalance - params.withdrawAmount; + assertEq(vars.actualLockupBalance, expectedLockupBalance, "post-withdraw Lockup balance"); + + // Assert that the contract's ETH balance has been updated. + assertEq(address(lockup).balance, vars.initialLockupBalanceETH + FEE, "post-withdraw Lockup balance ETH"); + + // Assert that the Recipient's balance has been updated. + uint256 expectedRecipientBalance = vars.initialRecipientBalance + params.withdrawAmount; + assertEq(vars.actualRecipientBalance, expectedRecipientBalance, "post-withdraw Recipient balance"); + } + } + + /*////////////////////////////////////////////////////////////////////////// + CANCEL HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev A shared cancel function to be used by all the fork tests. + function cancel(Params memory params) internal { + // Run the cancel tests only if the stream is cancelable and is neither depleted nor settled. + if (params.create.cancelable && !vars.isDepleted && !vars.isSettled) { + // Load the pre-cancel token balances. + uint256[] memory balances = getTokenBalances( + address(FORK_TOKEN), Solarray.addresses(address(lockup), params.create.sender, params.create.recipient) + ); + vars.initialLockupBalance = balances[0]; + vars.initialSenderBalance = balances[1]; + vars.initialRecipientBalance = balances[2]; + + // Expect the relevant events to be emitted. + vm.expectEmit({ emitter: address(lockup) }); + vars.senderAmount = lockup.refundableAmountOf(vars.streamId); + vars.recipientAmount = lockup.withdrawableAmountOf(vars.streamId); + emit ISablierLockupBase.CancelLockupStream( + vars.streamId, + params.create.sender, + params.create.recipient, + FORK_TOKEN, + vars.senderAmount, + vars.recipientAmount + ); + vm.expectEmit({ emitter: address(lockup) }); + emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); + + // Cancel the stream. + resetPrank({ msgSender: params.create.sender }); + uint128 refundedAmount = lockup.cancel(vars.streamId); + + // Assert that the refunded amount is correct. + assertEq(refundedAmount, vars.senderAmount, "refundedAmount"); + + // Assert that the stream's status is correct. + vars.expectedStatus = vars.recipientAmount > 0 ? Lockup.Status.CANCELED : Lockup.Status.DEPLETED; + assertEq(lockup.statusOf(vars.streamId), vars.expectedStatus, "post-cancel stream status"); + + // Load the post-cancel token balances. + balances = getTokenBalances( + address(FORK_TOKEN), Solarray.addresses(address(lockup), params.create.sender, params.create.recipient) + ); + vars.actualLockupBalance = balances[0]; + vars.actualSenderBalance = balances[1]; + vars.actualRecipientBalance = balances[2]; + + // Assert that the contract's balance has been updated. + uint256 expectedLockupBalance = vars.initialLockupBalance - vars.senderAmount; + assertEq(vars.actualLockupBalance, expectedLockupBalance, "post-cancel Lockup balance"); + + // Assert that the Sender's balance has been updated. + uint256 expectedSenderBalance = vars.initialSenderBalance + vars.senderAmount; + assertEq(vars.actualSenderBalance, expectedSenderBalance, "post-cancel Sender balance"); + + // Assert that the Recipient's balance has not changed. + assertEq(vars.actualRecipientBalance, vars.initialRecipientBalance, "post-cancel Recipient balance"); + } + + // Assert that the not burned NFT. + assertEq(lockup.ownerOf(vars.streamId), params.create.recipient, "post-cancel NFT owner"); + } +} diff --git a/tests/fork/LockupDynamic.t.sol b/tests/fork/LockupDynamic.t.sol index 2df1c2e47..18d19caed 100644 --- a/tests/fork/LockupDynamic.t.sol +++ b/tests/fork/LockupDynamic.t.sol @@ -3,82 +3,23 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { Solarray } from "solarray/src/Solarray.sol"; import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Lockup, LockupDynamic } from "src/types/DataTypes.sol"; -import { Fork_Test } from "./Fork.t.sol"; +import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup_Fork_Test } from "./Lockup.t.sol"; -abstract contract Lockup_Dynamic_Fork_Test is Fork_Test { +abstract contract Lockup_Dynamic_Fork_Test is Lockup_Fork_Test { /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - constructor(IERC20 forkToken) Fork_Test(forkToken) { } - - /*////////////////////////////////////////////////////////////////////////// - SET-UP FUNCTION - //////////////////////////////////////////////////////////////////////////*/ - - function setUp() public virtual override { - Fork_Test.setUp(); - - // Approve {SablierLockup} to transfer the holder's tokens. - // We use a low-level call to ignore reverts because the token can have the missing return value bug. - (bool success,) = address(FORK_TOKEN).call(abi.encodeCall(IERC20.approve, (address(lockup), MAX_UINT256))); - success; + constructor(IERC20 forkToken) Lockup_Fork_Test(forkToken) { + lockupModel = Lockup.Model.LOCKUP_DYNAMIC; } /*////////////////////////////////////////////////////////////////////////// TEST FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - struct Params { - address sender; - address recipient; - uint128 withdrawAmount; - uint40 startTime; - uint40 warpTimestamp; - LockupDynamic.Segment[] segments; - } - - struct Vars { - // Generic vars - address actualNFTOwner; - uint256 actualLockupBalance; - uint256 actualRecipientBalance; - Lockup.Status actualStatus; - uint256[] balances; - address expectedNFTOwner; - uint256 expectedLockupBalance; - uint256 expectedRecipientBalance; - Lockup.Status expectedStatus; - uint256 initialLockupBalance; - uint256 initialRecipientBalance; - bool isCancelable; - bool isDepleted; - bool isSettled; - uint256 streamId; - Lockup.Timestamps timestamps; - // Create vars - uint256 actualHolderBalance; - uint256 actualNextStreamId; - uint256 expectedHolderBalance; - uint256 expectedNextStreamId; - uint128 depositAmount; - // Withdraw vars - uint128 actualWithdrawnAmount; - uint128 expectedWithdrawnAmount; - uint256 initialLockupBalanceETH; - uint128 withdrawableAmount; - // Cancel vars - uint256 actualSenderBalance; - uint256 expectedSenderBalance; - uint256 initialSenderBalance; - uint128 recipientAmount; - uint128 senderAmount; - } - /// @dev Checklist: /// /// - It should perform all expected ERC-20 transfers @@ -103,32 +44,14 @@ abstract contract Lockup_Dynamic_Fork_Test is Fork_Test { /// - Multiple values for the withdraw amount, including zero /// - The whole gamut of stream statuses function testForkFuzz_CreateWithdrawCancel(Params memory params) external { - checkUsers(params.sender, params.recipient, address(lockup)); - vm.assume(params.segments.length != 0); - params.startTime = boundUint40(params.startTime, 1, getBlockTimestamp() + 2 days); - - // Fuzz the segment timestamps. - fuzzSegmentTimestamps(params.segments, params.startTime); - - // Fuzz the segment amounts and calculate the deposit amount. - Vars memory vars; - vars.depositAmount = - fuzzDynamicStreamAmounts({ upperBound: uint128(initialHolderBalance), segments: params.segments }); - - // Make the holder the caller. - resetPrank(forkTokenHolder); - /*////////////////////////////////////////////////////////////////////////// CREATE //////////////////////////////////////////////////////////////////////////*/ - // Load the pre-create token balances. - vars.balances = getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup))); - vars.initialLockupBalance = vars.balances[0]; + vm.assume(params.segments.length != 0); - vars.streamId = lockup.nextStreamId(); - vars.timestamps = - Lockup.Timestamps({ start: params.startTime, end: params.segments[params.segments.length - 1].timestamp }); + // Bound the fuzzed parameters and load values into `vars`. + preCreateStream(params); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); @@ -136,226 +59,48 @@ abstract contract Lockup_Dynamic_Fork_Test is Fork_Test { vm.expectEmit({ emitter: address(lockup) }); emit ISablierLockup.CreateLockupDynamicStream({ streamId: vars.streamId, - commonParams: Lockup.CreateEventCommon({ - funder: forkTokenHolder, - sender: params.sender, - recipient: params.recipient, - depositAmount: vars.depositAmount, - token: FORK_TOKEN, - cancelable: true, - transferable: true, - timestamps: vars.timestamps, - shape: "Dynamic Shape" - }), + commonParams: defaults.lockupCreateEvent({ funder: forkTokenHolder, params: params.create, token_: FORK_TOKEN }), segments: params.segments }); // Create the stream. - lockup.createWithTimestampsLD( - Lockup.CreateWithTimestamps({ - sender: params.sender, - recipient: params.recipient, - depositAmount: vars.depositAmount, - token: FORK_TOKEN, - cancelable: true, - transferable: true, - timestamps: vars.timestamps, - shape: "Dynamic Shape" - }), - params.segments - ); + lockup.createWithTimestampsLD(params.create, params.segments); - // Check if the stream is settled. It is possible for a stream to settle at the time of creation because some - // segment amounts can be zero or the last segment timestamp can be in the past. - vars.isSettled = lockup.refundableAmountOf(vars.streamId) == 0 || vars.timestamps.end <= getBlockTimestamp(); - vars.isCancelable = vars.isSettled ? false : true; - - // Assert that the stream has been created. - assertEq(lockup.getDepositedAmount(vars.streamId), vars.depositAmount, "depositedAmount"); - assertEq(lockup.getEndTime(vars.streamId), vars.timestamps.end, "endTime"); - assertEq(lockup.isCancelable(vars.streamId), vars.isCancelable, "isCancelable"); - assertFalse(lockup.isDepleted(vars.streamId), "isDepleted"); - assertTrue(lockup.isStream(vars.streamId), "isStream"); - assertTrue(lockup.isTransferable(vars.streamId), "isTransferable"); - assertEq(lockup.getRecipient(vars.streamId), params.recipient, "recipient"); - assertEq(lockup.getSender(vars.streamId), params.sender, "sender"); + // Assert that the stream is created with the correct parameters. + assertEq({ lockup: lockup, streamId: vars.streamId, expectedLockup: params.create }); assertEq(lockup.getSegments(vars.streamId), params.segments); - assertEq(lockup.getStartTime(vars.streamId), params.startTime, "startTime"); - assertEq(lockup.getUnderlyingToken(vars.streamId), FORK_TOKEN, "underlyingToken"); - assertFalse(lockup.wasCanceled(vars.streamId), "wasCanceled"); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - if (params.startTime > getBlockTimestamp()) { - vars.expectedStatus = Lockup.Status.PENDING; - } else if (vars.isSettled) { - vars.expectedStatus = Lockup.Status.SETTLED; - } else { - vars.expectedStatus = Lockup.Status.STREAMING; - } - assertEq(vars.actualStatus, vars.expectedStatus, "post-create stream status"); - - // Assert that the next stream ID has been bumped. - vars.actualNextStreamId = lockup.nextStreamId(); - vars.expectedNextStreamId = vars.streamId + 1; - assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "post-create nextStreamId"); - - // Assert that the NFT has been minted. - vars.actualNFTOwner = lockup.ownerOf({ tokenId: vars.streamId }); - vars.expectedNFTOwner = params.recipient; - assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "post-create NFT owner"); - - // Load the post-create token balances. - vars.balances = getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), forkTokenHolder)); - vars.actualLockupBalance = vars.balances[0]; - vars.actualHolderBalance = vars.balances[1]; - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance + vars.depositAmount; - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-create Lockup contract balance"); + // Update the streamed amount. + vars.streamedAmount = + calculateStreamedAmountLD(params.segments, params.create.timestamps.start, params.create.depositAmount); - // Assert that the holder's balance has been updated. - vars.expectedHolderBalance = initialHolderBalance - vars.depositAmount; - assertEq(vars.actualHolderBalance, vars.expectedHolderBalance, "post-create Holder balance"); + // Run post-create assertions and update token balances in `vars`. + postCreateStream(params); /*////////////////////////////////////////////////////////////////////////// WITHDRAW //////////////////////////////////////////////////////////////////////////*/ + // Bound the warp timestamp. + params.warpTimestamp = boundUint40( + params.warpTimestamp, params.create.timestamps.start + 1 seconds, params.create.timestamps.end + 100 seconds + ); + // Simulate the passage of time. - params.warpTimestamp = - boundUint40(params.warpTimestamp, vars.timestamps.start, vars.timestamps.end + 100 seconds); vm.warp({ newTimestamp: params.warpTimestamp }); - // Bound the withdraw amount. - vars.withdrawableAmount = lockup.withdrawableAmountOf(vars.streamId); - params.withdrawAmount = boundUint128(params.withdrawAmount, 0, vars.withdrawableAmount); - - // Check if the stream has settled or will get depleted. It is possible for the stream to be just settled - // and not depleted because the withdraw amount is fuzzed. - vars.isDepleted = params.withdrawAmount == vars.depositAmount; - vars.isSettled = lockup.refundableAmountOf(vars.streamId) == 0; - - // Only run the withdraw tests if the withdraw amount is not zero. - if (params.withdrawAmount > 0) { - // Load the pre-withdraw token balances. - vars.initialLockupBalance = vars.actualLockupBalance; - vars.initialLockupBalanceETH = address(lockup).balance; - vars.initialRecipientBalance = FORK_TOKEN.balanceOf(params.recipient); - - // Expect the relevant events to be emitted. - vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: vars.streamId, - to: params.recipient, - token: FORK_TOKEN, - amount: params.withdrawAmount - }); - vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); - - // Make the withdrawal. - resetPrank({ msgSender: params.recipient }); - vm.deal({ account: params.recipient, newBalance: 100 ether }); - lockup.withdraw{ value: FEE }({ - streamId: vars.streamId, - to: params.recipient, - amount: params.withdrawAmount - }); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - if (vars.isDepleted) { - vars.expectedStatus = Lockup.Status.DEPLETED; - } else if (vars.isSettled) { - vars.expectedStatus = Lockup.Status.SETTLED; - } else { - vars.expectedStatus = Lockup.Status.STREAMING; - } - assertEq(vars.actualStatus, vars.expectedStatus, "post-withdraw stream status"); - - // Assert that the withdrawn amount has been updated. - vars.actualWithdrawnAmount = lockup.getWithdrawnAmount(vars.streamId); - vars.expectedWithdrawnAmount = params.withdrawAmount; - assertEq(vars.actualWithdrawnAmount, vars.expectedWithdrawnAmount, "post-withdraw withdrawnAmount"); - - // Load the post-withdraw token balances. - vars.balances = getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), params.recipient)); - vars.actualLockupBalance = vars.balances[0]; - vars.actualRecipientBalance = vars.balances[1]; - - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance - uint256(params.withdrawAmount); - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-withdraw Lockup contract balance"); + // Update the streamed amount. + vars.streamedAmount = + calculateStreamedAmountLD(params.segments, params.create.timestamps.start, params.create.depositAmount); - // Assert that the contract's ETH balance has been updated. - assertEq(address(lockup).balance, vars.initialLockupBalanceETH + FEE, "post-withdraw Lockup balance ETH"); - - // Assert that the Recipient's balance has been updated. - vars.expectedRecipientBalance = vars.initialRecipientBalance + uint256(params.withdrawAmount); - assertEq(vars.actualRecipientBalance, vars.expectedRecipientBalance, "post-withdraw Recipient balance"); - } + // Run the fork test for withdraw function and update the parameters. + withdraw(params); /*////////////////////////////////////////////////////////////////////////// CANCEL //////////////////////////////////////////////////////////////////////////*/ - // Only run the cancel tests if the stream is neither depleted nor settled. - if (!vars.isDepleted && !vars.isSettled) { - // Load the pre-cancel token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), params.sender, params.recipient) - ); - vars.initialLockupBalance = vars.balances[0]; - vars.initialSenderBalance = vars.balances[1]; - vars.initialRecipientBalance = vars.balances[2]; - - // Expect the relevant events to be emitted. - vm.expectEmit({ emitter: address(lockup) }); - vars.senderAmount = lockup.refundableAmountOf(vars.streamId); - vars.recipientAmount = lockup.withdrawableAmountOf(vars.streamId); - emit ISablierLockupBase.CancelLockupStream( - vars.streamId, params.sender, params.recipient, FORK_TOKEN, vars.senderAmount, vars.recipientAmount - ); - vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); - - // Cancel the stream. - resetPrank({ msgSender: params.sender }); - uint128 refundedAmount = lockup.cancel(vars.streamId); - - // Assert that the refunded amount is correct. - assertEq(refundedAmount, vars.senderAmount, "refundedAmount"); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - vars.expectedStatus = vars.recipientAmount > 0 ? Lockup.Status.CANCELED : Lockup.Status.DEPLETED; - assertEq(vars.actualStatus, vars.expectedStatus, "post-cancel stream status"); - - // Load the post-cancel token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), params.sender, params.recipient) - ); - vars.actualLockupBalance = vars.balances[0]; - vars.actualSenderBalance = vars.balances[1]; - vars.actualRecipientBalance = vars.balances[2]; - - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance - uint256(vars.senderAmount); - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-cancel lockup contract balance"); - - // Assert that the Sender's balance has been updated. - vars.expectedSenderBalance = vars.initialSenderBalance + uint256(vars.senderAmount); - assertEq(vars.actualSenderBalance, vars.expectedSenderBalance, "post-cancel Sender balance"); - - // Assert that the Recipient's balance has not changed. - vars.expectedRecipientBalance = vars.initialRecipientBalance; - assertEq(vars.actualRecipientBalance, vars.expectedRecipientBalance, "post-cancel Recipient balance"); - } - - // Assert that the not burned NFT. - vars.actualNFTOwner = lockup.ownerOf({ tokenId: vars.streamId }); - vars.expectedNFTOwner = params.recipient; - assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "post-cancel NFT owner"); + // Run the cancel test. + cancel(params); } } diff --git a/tests/fork/LockupLinear.t.sol b/tests/fork/LockupLinear.t.sol index 53947b810..9fe162e26 100644 --- a/tests/fork/LockupLinear.t.sol +++ b/tests/fork/LockupLinear.t.sol @@ -3,87 +3,24 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { Solarray } from "solarray/src/Solarray.sol"; import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Lockup, LockupLinear } from "src/types/DataTypes.sol"; -import { Fork_Test } from "./Fork.t.sol"; +import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup_Fork_Test } from "./Lockup.t.sol"; -abstract contract Lockup_Linear_Fork_Test is Fork_Test { +abstract contract Lockup_Linear_Fork_Test is Lockup_Fork_Test { /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - constructor(IERC20 forkToken) Fork_Test(forkToken) { } - - /*////////////////////////////////////////////////////////////////////////// - SET-UP FUNCTION - //////////////////////////////////////////////////////////////////////////*/ - - function setUp() public virtual override { - Fork_Test.setUp(); - - // Approve {SablierLockup} to transfer the token holder's tokens. - // We use a low-level call to ignore reverts because the token can have the missing return value bug. - (bool success,) = address(FORK_TOKEN).call(abi.encodeCall(IERC20.approve, (address(lockup), MAX_UINT256))); - success; + constructor(IERC20 forkToken) Lockup_Fork_Test(forkToken) { + lockupModel = Lockup.Model.LOCKUP_LINEAR; } /*////////////////////////////////////////////////////////////////////////// TEST FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - struct Params { - address sender; - address recipient; - uint128 depositAmount; - uint128 withdrawAmount; - uint40 warpTimestamp; - Lockup.Timestamps timestamps; - LockupLinear.UnlockAmounts unlockAmounts; - uint40 cliffTime; - } - - struct Vars { - // Generic vars - uint256 actualLockupBalance; - uint256 actualHolderBalance; - address actualNFTOwner; - uint256 actualRecipientBalance; - Lockup.Status actualStatus; - uint256[] balances; - uint40 blockTimestamp; - uint40 endTimeLowerBound; - uint256 expectedLockupBalance; - uint256 expectedHolderBalance; - address expectedNFTOwner; - uint256 expectedRecipientBalance; - Lockup.Status expectedStatus; - bool hasCliff; - uint256 initialLockupBalance; - uint256 initialRecipientBalance; - bool isCancelable; - bool isDepleted; - bool isSettled; - uint256 streamId; - uint128 streamedAmount; - // Create vars - uint256 actualNextStreamId; - uint256 expectedNextStreamId; - // Withdraw vars - uint128 actualWithdrawnAmount; - uint128 expectedWithdrawnAmount; - uint256 initialLockupBalanceETH; - uint128 withdrawableAmount; - // Cancel vars - uint256 actualSenderBalance; - uint256 expectedSenderBalance; - uint256 initialSenderBalance; - uint128 recipientAmount; - uint128 senderAmount; - } - /// @dev Checklist: /// /// - It should perform all expected ERC-20 transfers @@ -110,45 +47,12 @@ abstract contract Lockup_Linear_Fork_Test is Fork_Test { /// - Cliff time zero and not zero /// - The whole gamut of stream statuses function testForkFuzz_CreateWithdrawCancel(Params memory params) external { - checkUsers(params.sender, params.recipient, address(lockup)); - - // Bound the parameters. - Vars memory vars; - vars.blockTimestamp = getBlockTimestamp(); - params.timestamps.start = boundUint40( - params.timestamps.start, vars.blockTimestamp - 1000 seconds, vars.blockTimestamp + 10_000 seconds - ); - params.depositAmount = boundUint128(params.depositAmount, 1, uint128(initialHolderBalance)); - - // The cliff time must be either zero or greater than the start time. - vars.hasCliff = params.cliffTime > 0; - params.cliffTime = vars.hasCliff - ? boundUint40(params.cliffTime, params.timestamps.start + 1 seconds, params.timestamps.start + 52 weeks) - : 0; - - // Bound the end time so that it is always greater than the start time, and the cliff time. - vars.endTimeLowerBound = maxOfTwo(params.timestamps.start, params.cliffTime); - params.timestamps.end = - boundUint40(params.timestamps.end, vars.endTimeLowerBound + 1 seconds, MAX_UNIX_TIMESTAMP); - - // Bound the unlock amounts. - params.unlockAmounts.start = boundUint128(params.unlockAmounts.start, 0, params.depositAmount); - params.unlockAmounts.cliff = vars.hasCliff - ? boundUint128(params.unlockAmounts.cliff, 0, params.depositAmount - params.unlockAmounts.start) - : 0; - - // Make the holder the caller. - resetPrank(forkTokenHolder); - /*////////////////////////////////////////////////////////////////////////// CREATE //////////////////////////////////////////////////////////////////////////*/ - // Load the pre-create token balances. - vars.balances = getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup))); - vars.initialLockupBalance = vars.balances[0]; - - vars.streamId = lockup.nextStreamId(); + // Bound the fuzzed parameters and load values into `vars`. + preCreateStream(params); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); @@ -156,241 +60,68 @@ abstract contract Lockup_Linear_Fork_Test is Fork_Test { vm.expectEmit({ emitter: address(lockup) }); emit ISablierLockup.CreateLockupLinearStream({ streamId: vars.streamId, - commonParams: Lockup.CreateEventCommon({ - funder: forkTokenHolder, - sender: params.sender, - recipient: params.recipient, - depositAmount: params.depositAmount, - token: FORK_TOKEN, - cancelable: true, - transferable: true, - timestamps: params.timestamps, - shape: "Linear Shape" - }), + commonParams: defaults.lockupCreateEvent({ funder: forkTokenHolder, params: params.create, token_: FORK_TOKEN }), cliffTime: params.cliffTime, unlockAmounts: params.unlockAmounts }); // Create the stream. - lockup.createWithTimestampsLL( - Lockup.CreateWithTimestamps({ - sender: params.sender, - recipient: params.recipient, - depositAmount: params.depositAmount, - token: FORK_TOKEN, - cancelable: true, - transferable: true, - timestamps: params.timestamps, - shape: "Linear Shape" - }), - params.unlockAmounts, - params.cliffTime - ); - - vars.streamedAmount = calculateStreamedAmountLL( - params.timestamps.start, params.cliffTime, params.timestamps.end, params.depositAmount, params.unlockAmounts - ); - - // Check if the stream is settled. It is possible for a stream to settle at the time of creation in case 1. the - // start unlock amount equals the deposited amount 2. end time is in the past. - if (vars.streamedAmount == params.depositAmount) { - vars.isSettled = true; - } else { - vars.isSettled = false; - } - vars.isCancelable = vars.isSettled ? false : true; + lockup.createWithTimestampsLL(params.create, params.unlockAmounts, params.cliffTime); - // Assert that the stream has been created. + // Assert that the stream is created with the correct parameters. + assertEq({ lockup: lockup, streamId: vars.streamId, expectedLockup: params.create }); assertEq(lockup.getCliffTime(vars.streamId), params.cliffTime, "cliffTime"); - assertEq(lockup.getDepositedAmount(vars.streamId), params.depositAmount, "depositedAmount"); - assertEq(lockup.isCancelable(vars.streamId), vars.isCancelable, "isCancelable"); - assertFalse(lockup.isDepleted(vars.streamId), "isDepleted"); - assertTrue(lockup.isStream(vars.streamId), "isStream"); - assertTrue(lockup.isTransferable(vars.streamId), "isTransferable"); - assertEq(lockup.getEndTime(vars.streamId), params.timestamps.end, "endTime"); - assertEq(lockup.getRecipient(vars.streamId), params.recipient, "recipient"); - assertEq(lockup.getSender(vars.streamId), params.sender, "sender"); - assertEq(lockup.getStartTime(vars.streamId), params.timestamps.start, "startTime"); - assertEq(lockup.getUnderlyingToken(vars.streamId), FORK_TOKEN, "underlyingToken"); - assertEq(lockup.getUnlockAmounts(vars.streamId).start, params.unlockAmounts.start, "unlockAmounts.start"); - assertEq(lockup.getUnlockAmounts(vars.streamId).cliff, params.unlockAmounts.cliff, "unlockAmounts.cliff"); - assertFalse(lockup.wasCanceled(vars.streamId), "wasCanceled"); + assertEq(lockup.getUnlockAmounts(vars.streamId), params.unlockAmounts); + assertEq(lockup.getLockupModel(vars.streamId), Lockup.Model.LOCKUP_LINEAR); - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - if (vars.streamedAmount == params.depositAmount) { - vars.expectedStatus = Lockup.Status.SETTLED; - } else if (params.timestamps.start > vars.blockTimestamp) { - vars.expectedStatus = Lockup.Status.PENDING; - } else { - vars.expectedStatus = Lockup.Status.STREAMING; - } - assertEq(vars.actualStatus, vars.expectedStatus, "post-create stream status"); - - // Assert that the next stream ID has been bumped. - vars.actualNextStreamId = lockup.nextStreamId(); - vars.expectedNextStreamId = vars.streamId + 1; - assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "post-create nextStreamId"); - - // Assert that the NFT has been minted. - vars.actualNFTOwner = lockup.ownerOf({ tokenId: vars.streamId }); - vars.expectedNFTOwner = params.recipient; - assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "post-create NFT owner"); - - // Load the post-create token balances. - vars.balances = getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), forkTokenHolder)); - vars.actualLockupBalance = vars.balances[0]; - vars.actualHolderBalance = vars.balances[1]; - - // Assert that the Lockup contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance + params.depositAmount; - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-create Lockup balance"); + // Update the streamed amount. + vars.streamedAmount = calculateStreamedAmountLL( + params.create.timestamps.start, + params.cliffTime, + params.create.timestamps.end, + params.create.depositAmount, + params.unlockAmounts + ); - // Assert that the holder's balance has been updated. - vars.expectedHolderBalance = initialHolderBalance - params.depositAmount; - assertEq(vars.actualHolderBalance, vars.expectedHolderBalance, "post-create Holder balance"); + // Run post-create assertions and update token balances in `vars`. + postCreateStream(params); /*////////////////////////////////////////////////////////////////////////// WITHDRAW //////////////////////////////////////////////////////////////////////////*/ + // Bound the warp timestamp according to the cliff status, if it exists. + if (vars.hasCliff) { + params.warpTimestamp = + boundUint40(params.warpTimestamp, params.cliffTime, params.create.timestamps.end + 100 seconds); + } else { + params.warpTimestamp = boundUint40( + params.warpTimestamp, + params.create.timestamps.start + 1 seconds, + params.create.timestamps.end + 100 seconds + ); + } + // Simulate the passage of time. - params.warpTimestamp = boundUint40( - params.warpTimestamp, - vars.hasCliff ? params.cliffTime : params.timestamps.start + 1 seconds, - params.timestamps.end + 100 seconds - ); vm.warp({ newTimestamp: params.warpTimestamp }); - // Bound the withdraw amount. - vars.withdrawableAmount = lockup.withdrawableAmountOf(vars.streamId); - params.withdrawAmount = boundUint128(params.withdrawAmount, 0, vars.withdrawableAmount); - - // Check if the stream has settled or will get depleted. It is possible for the stream to be just settled - // and not depleted because the withdraw amount is fuzzed. - vars.isSettled = lockup.refundableAmountOf(vars.streamId) == 0; - vars.isDepleted = params.withdrawAmount == params.depositAmount; - - // Only run the withdraw tests if the withdraw amount is not zero. - if (params.withdrawAmount > 0) { - // Load the pre-withdraw token balances. - vars.initialLockupBalance = vars.actualLockupBalance; - vars.initialLockupBalanceETH = address(lockup).balance; - vars.initialRecipientBalance = FORK_TOKEN.balanceOf(params.recipient); - - // Expect the relevant events to be emitted. - vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: vars.streamId, - to: params.recipient, - token: FORK_TOKEN, - amount: params.withdrawAmount - }); - vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); - - // Make the withdrawal and pay a fee. - resetPrank({ msgSender: params.recipient }); - vm.deal({ account: params.recipient, newBalance: 100 ether }); - lockup.withdraw{ value: FEE }({ - streamId: vars.streamId, - to: params.recipient, - amount: params.withdrawAmount - }); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - if (vars.isDepleted) { - vars.expectedStatus = Lockup.Status.DEPLETED; - } else if (vars.isSettled) { - vars.expectedStatus = Lockup.Status.SETTLED; - } else { - vars.expectedStatus = Lockup.Status.STREAMING; - } - assertEq(vars.actualStatus, vars.expectedStatus, "post-withdraw stream status"); - - // Assert that the withdrawn amount has been updated. - vars.actualWithdrawnAmount = lockup.getWithdrawnAmount(vars.streamId); - vars.expectedWithdrawnAmount = params.withdrawAmount; - assertEq(vars.actualWithdrawnAmount, vars.expectedWithdrawnAmount, "post-withdraw withdrawnAmount"); - - // Load the post-withdraw token balances. - vars.balances = getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), params.recipient)); - vars.actualLockupBalance = vars.balances[0]; - vars.actualRecipientBalance = vars.balances[1]; - - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance - uint256(params.withdrawAmount); - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-withdraw Lockup balance"); - - // Assert that the contract's ETH balance has been updated. - assertEq(address(lockup).balance, vars.initialLockupBalanceETH + FEE, "post-withdraw Lockup balance ETH"); + // Update the streamed amount. + vars.streamedAmount = calculateStreamedAmountLL( + params.create.timestamps.start, + params.cliffTime, + params.create.timestamps.end, + params.create.depositAmount, + params.unlockAmounts + ); - // Assert that the Recipient's balance has been updated. - vars.expectedRecipientBalance = vars.initialRecipientBalance + uint256(params.withdrawAmount); - assertEq(vars.actualRecipientBalance, vars.expectedRecipientBalance, "post-withdraw Recipient balance"); - } + // Run the fork test for withdraw function and update the parameters. + withdraw(params); /*////////////////////////////////////////////////////////////////////////// CANCEL //////////////////////////////////////////////////////////////////////////*/ - // Only run the cancel tests if the stream is neither depleted nor settled. - if (!vars.isDepleted && !vars.isSettled) { - // Load the pre-cancel token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), params.sender, params.recipient) - ); - vars.initialLockupBalance = vars.balances[0]; - vars.initialSenderBalance = vars.balances[1]; - vars.initialRecipientBalance = vars.balances[2]; - - // Expect the relevant events to be emitted. - vm.expectEmit({ emitter: address(lockup) }); - vars.senderAmount = lockup.refundableAmountOf(vars.streamId); - vars.recipientAmount = lockup.withdrawableAmountOf(vars.streamId); - emit ISablierLockupBase.CancelLockupStream( - vars.streamId, params.sender, params.recipient, FORK_TOKEN, vars.senderAmount, vars.recipientAmount - ); - vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); - - // Cancel the stream. - resetPrank({ msgSender: params.sender }); - uint128 refundedAmount = lockup.cancel(vars.streamId); - - // Assert that the refunded amount is correct. - assertEq(refundedAmount, vars.senderAmount, "refundedAmount"); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - vars.expectedStatus = vars.recipientAmount > 0 ? Lockup.Status.CANCELED : Lockup.Status.DEPLETED; - assertEq(vars.actualStatus, vars.expectedStatus, "post-cancel stream status"); - - // Load the post-cancel token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), params.sender, params.recipient) - ); - vars.actualLockupBalance = vars.balances[0]; - vars.actualSenderBalance = vars.balances[1]; - vars.actualRecipientBalance = vars.balances[2]; - - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance - uint256(vars.senderAmount); - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-cancel Lockup balance"); - - // Assert that the Sender's balance has been updated. - vars.expectedSenderBalance = vars.initialSenderBalance + uint256(vars.senderAmount); - assertEq(vars.actualSenderBalance, vars.expectedSenderBalance, "post-cancel Sender balance"); - - // Assert that the Recipient's balance has not changed. - vars.expectedRecipientBalance = vars.initialRecipientBalance; - assertEq(vars.actualRecipientBalance, vars.expectedRecipientBalance, "post-cancel Recipient balance"); - } - - // Assert that the not burned NFT. - vars.actualNFTOwner = lockup.ownerOf({ tokenId: vars.streamId }); - vars.expectedNFTOwner = params.recipient; - assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "post-cancel NFT owner"); + // Run the cancel test. + cancel(params); } } diff --git a/tests/fork/LockupTranched.t.sol b/tests/fork/LockupTranched.t.sol index a5cb5ee5c..5a7f319ac 100644 --- a/tests/fork/LockupTranched.t.sol +++ b/tests/fork/LockupTranched.t.sol @@ -3,82 +3,23 @@ pragma solidity >=0.8.22 <0.9.0; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { Solarray } from "solarray/src/Solarray.sol"; import { ISablierLockup } from "src/interfaces/ISablierLockup.sol"; -import { ISablierLockupBase } from "src/interfaces/ISablierLockupBase.sol"; -import { Lockup, LockupTranched } from "src/types/DataTypes.sol"; -import { Fork_Test } from "./Fork.t.sol"; +import { Lockup } from "src/types/DataTypes.sol"; +import { Lockup_Fork_Test } from "./Lockup.t.sol"; -abstract contract Lockup_Tranched_Fork_Test is Fork_Test { +abstract contract Lockup_Tranched_Fork_Test is Lockup_Fork_Test { /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - constructor(IERC20 forkToken) Fork_Test(forkToken) { } - - /*////////////////////////////////////////////////////////////////////////// - SET-UP FUNCTION - //////////////////////////////////////////////////////////////////////////*/ - - function setUp() public virtual override { - Fork_Test.setUp(); - - // Approve {SablierLockup} to transfer the holder's tokens. - // We use a low-level call to ignore reverts because the token can have the missing return value bug. - (bool success,) = address(FORK_TOKEN).call(abi.encodeCall(IERC20.approve, (address(lockup), MAX_UINT256))); - success; + constructor(IERC20 forkToken) Lockup_Fork_Test(forkToken) { + lockupModel = Lockup.Model.LOCKUP_TRANCHED; } /*////////////////////////////////////////////////////////////////////////// TEST FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - struct Params { - address sender; - address recipient; - uint128 withdrawAmount; - uint40 startTime; - uint40 warpTimestamp; - LockupTranched.Tranche[] tranches; - } - - struct Vars { - // Generic vars - address actualNFTOwner; - uint256 actualLockupBalance; - uint256 actualRecipientBalance; - Lockup.Status actualStatus; - uint256[] balances; - address expectedNFTOwner; - uint256 expectedLockupBalance; - uint256 expectedRecipientBalance; - Lockup.Status expectedStatus; - uint256 initialLockupBalance; - uint256 initialRecipientBalance; - bool isCancelable; - bool isDepleted; - bool isSettled; - uint256 streamId; - Lockup.Timestamps timestamps; - // Create vars - uint256 actualHolderBalance; - uint256 actualNextStreamId; - uint256 expectedHolderBalance; - uint256 expectedNextStreamId; - uint128 depositAmount; - // Withdraw vars - uint128 actualWithdrawnAmount; - uint128 expectedWithdrawnAmount; - uint256 initialLockupBalanceETH; - uint128 withdrawableAmount; - // Cancel vars - uint256 actualSenderBalance; - uint256 expectedSenderBalance; - uint256 initialSenderBalance; - uint128 recipientAmount; - uint128 senderAmount; - } - /// @dev Checklist: /// /// - It should perform all expected ERC-20 transfers @@ -103,32 +44,15 @@ abstract contract Lockup_Tranched_Fork_Test is Fork_Test { /// - Multiple values for the withdraw amount, including zero /// - The whole gamut of stream statuses function testForkFuzz_CreateWithdrawCancel(Params memory params) external { - checkUsers(params.sender, params.recipient, address(lockup)); - vm.assume(params.tranches.length != 0); - params.startTime = boundUint40(params.startTime, 1, getBlockTimestamp() + 2 days); - - // Fuzz the tranche timestamps. - fuzzTrancheTimestamps(params.tranches, params.startTime); - - // Fuzz the tranche amounts and calculate the deposit amount. - Vars memory vars; - vars.depositAmount = - fuzzTranchedStreamAmounts({ upperBound: uint128(initialHolderBalance), tranches: params.tranches }); - - // Make the holder the caller. - resetPrank(forkTokenHolder); - /*////////////////////////////////////////////////////////////////////////// CREATE //////////////////////////////////////////////////////////////////////////*/ - // Load the pre-create token balances. - vars.balances = getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup))); - vars.initialLockupBalance = vars.balances[0]; + // Fuzz the tranche timestamps. + vm.assume(params.tranches.length != 0); - vars.streamId = lockup.nextStreamId(); - vars.timestamps = - Lockup.Timestamps({ start: params.startTime, end: params.tranches[params.tranches.length - 1].timestamp }); + // Bound the fuzzed parameters and load values into `vars`. + preCreateStream(params); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); @@ -136,226 +60,46 @@ abstract contract Lockup_Tranched_Fork_Test is Fork_Test { vm.expectEmit({ emitter: address(lockup) }); emit ISablierLockup.CreateLockupTranchedStream({ streamId: vars.streamId, - commonParams: Lockup.CreateEventCommon({ - funder: forkTokenHolder, - sender: params.sender, - recipient: params.recipient, - depositAmount: vars.depositAmount, - token: FORK_TOKEN, - cancelable: true, - transferable: true, - timestamps: vars.timestamps, - shape: "Tranched Shape" - }), + commonParams: defaults.lockupCreateEvent({ funder: forkTokenHolder, params: params.create, token_: FORK_TOKEN }), tranches: params.tranches }); // Create the stream. - lockup.createWithTimestampsLT( - Lockup.CreateWithTimestamps({ - sender: params.sender, - recipient: params.recipient, - depositAmount: vars.depositAmount, - token: FORK_TOKEN, - cancelable: true, - transferable: true, - timestamps: vars.timestamps, - shape: "Tranched Shape" - }), - params.tranches - ); - - // Check if the stream is settled. It is possible for a stream to settle at the time of creation because some - // tranche amounts can be zero or the last tranche timestamp can be in the past - vars.isSettled = lockup.refundableAmountOf(vars.streamId) == 0 || vars.timestamps.end <= getBlockTimestamp(); - vars.isCancelable = vars.isSettled ? false : true; + lockup.createWithTimestampsLT(params.create, params.tranches); - // Assert that the stream has been created. - assertEq(lockup.isCancelable(vars.streamId), vars.isCancelable, "isCancelable"); - assertFalse(lockup.isDepleted(vars.streamId), "isDepleted"); - assertTrue(lockup.isStream(vars.streamId), "isStream"); - assertTrue(lockup.isTransferable(vars.streamId), "isTransferable"); - assertEq(lockup.getDepositedAmount(vars.streamId), vars.depositAmount, "depositedAmount"); - assertEq(lockup.getEndTime(vars.streamId), vars.timestamps.end, "endTime"); - assertEq(lockup.getRecipient(vars.streamId), params.recipient, "recipient"); - assertEq(lockup.getSender(vars.streamId), params.sender, "sender"); - assertEq(lockup.getStartTime(vars.streamId), params.startTime, "startTime"); + // Assert that the stream is created with the correct parameters. + assertEq({ streamId: vars.streamId, lockup: lockup, expectedLockup: params.create }); assertEq(lockup.getTranches(vars.streamId), params.tranches); - assertEq(lockup.getUnderlyingToken(vars.streamId), FORK_TOKEN, "underlyingToken"); - assertFalse(lockup.wasCanceled(vars.streamId), "wasCanceled"); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - if (params.startTime > getBlockTimestamp()) { - vars.expectedStatus = Lockup.Status.PENDING; - } else if (vars.isSettled) { - vars.expectedStatus = Lockup.Status.SETTLED; - } else { - vars.expectedStatus = Lockup.Status.STREAMING; - } - assertEq(vars.actualStatus, vars.expectedStatus, "post-create stream status"); - - // Assert that the next stream ID has been bumped. - vars.actualNextStreamId = lockup.nextStreamId(); - vars.expectedNextStreamId = vars.streamId + 1; - assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "post-create nextStreamId"); - - // Assert that the NFT has been minted. - vars.actualNFTOwner = lockup.ownerOf({ tokenId: vars.streamId }); - vars.expectedNFTOwner = params.recipient; - assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "post-create NFT owner"); - // Load the post-create token balances. - vars.balances = getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), forkTokenHolder)); - vars.actualLockupBalance = vars.balances[0]; - vars.actualHolderBalance = vars.balances[1]; + // Update the streamed amount. + vars.streamedAmount = calculateStreamedAmountLT(params.tranches, params.create.depositAmount); - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance + vars.depositAmount; - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-create Lockup contract balance"); - - // Assert that the holder's balance has been updated. - vars.expectedHolderBalance = initialHolderBalance - vars.depositAmount; - assertEq(vars.actualHolderBalance, vars.expectedHolderBalance, "post-create Holder balance"); + // Run post-create assertions and update token balances in `vars`. + postCreateStream(params); /*////////////////////////////////////////////////////////////////////////// WITHDRAW //////////////////////////////////////////////////////////////////////////*/ + // Bound the warp timestamp. + params.warpTimestamp = boundUint40( + params.warpTimestamp, params.create.timestamps.start, params.create.timestamps.end + 100 seconds + ); + // Simulate the passage of time. - params.warpTimestamp = - boundUint40(params.warpTimestamp, vars.timestamps.start, vars.timestamps.end + 100 seconds); vm.warp({ newTimestamp: params.warpTimestamp }); - // Bound the withdraw amount. - vars.withdrawableAmount = lockup.withdrawableAmountOf(vars.streamId); - params.withdrawAmount = boundUint128(params.withdrawAmount, 0, vars.withdrawableAmount); - - // Check if the stream has settled or will get depleted. It is possible for the stream to be just settled - // and not depleted because the withdraw amount is fuzzed. - vars.isDepleted = params.withdrawAmount == vars.depositAmount; - vars.isSettled = lockup.refundableAmountOf(vars.streamId) == 0; - - // Only run the withdraw tests if the withdraw amount is not zero. - if (params.withdrawAmount > 0) { - // Load the pre-withdraw token balances. - vars.initialLockupBalance = vars.actualLockupBalance; - vars.initialLockupBalanceETH = address(lockup).balance; - vars.initialRecipientBalance = FORK_TOKEN.balanceOf(params.recipient); - - // Expect the relevant events to be emitted. - vm.expectEmit({ emitter: address(lockup) }); - emit ISablierLockupBase.WithdrawFromLockupStream({ - streamId: vars.streamId, - to: params.recipient, - token: FORK_TOKEN, - amount: params.withdrawAmount - }); - vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); - - // Make the withdrawal. - resetPrank({ msgSender: params.recipient }); - vm.deal({ account: params.recipient, newBalance: 100 ether }); - lockup.withdraw{ value: FEE }({ - streamId: vars.streamId, - to: params.recipient, - amount: params.withdrawAmount - }); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - if (vars.isDepleted) { - vars.expectedStatus = Lockup.Status.DEPLETED; - } else if (vars.isSettled) { - vars.expectedStatus = Lockup.Status.SETTLED; - } else { - vars.expectedStatus = Lockup.Status.STREAMING; - } - assertEq(vars.actualStatus, vars.expectedStatus, "post-withdraw stream status"); - - // Assert that the withdrawn amount has been updated. - vars.actualWithdrawnAmount = lockup.getWithdrawnAmount(vars.streamId); - vars.expectedWithdrawnAmount = params.withdrawAmount; - assertEq(vars.actualWithdrawnAmount, vars.expectedWithdrawnAmount, "post-withdraw withdrawnAmount"); - - // Load the post-withdraw token balances. - vars.balances = getTokenBalances(address(FORK_TOKEN), Solarray.addresses(address(lockup), params.recipient)); - vars.actualLockupBalance = vars.balances[0]; - vars.actualRecipientBalance = vars.balances[1]; - - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance - uint256(params.withdrawAmount); - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-withdraw Lockup contract balance"); + // Update the streamed amount. + vars.streamedAmount = calculateStreamedAmountLT(params.tranches, params.create.depositAmount); - // Assert that the contract's ETH balance has been updated. - assertEq(address(lockup).balance, vars.initialLockupBalanceETH + FEE, "post-withdraw Lockup balance ETH"); - - // Assert that the Recipient's balance has been updated. - vars.expectedRecipientBalance = vars.initialRecipientBalance + uint256(params.withdrawAmount); - assertEq(vars.actualRecipientBalance, vars.expectedRecipientBalance, "post-withdraw Recipient balance"); - } + // Run the fork test for withdraw function and update the parameters. + withdraw(params); /*////////////////////////////////////////////////////////////////////////// CANCEL //////////////////////////////////////////////////////////////////////////*/ - // Only run the cancel tests if the stream is neither depleted nor settled. - if (!vars.isDepleted && !vars.isSettled) { - // Load the pre-cancel token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), params.sender, params.recipient) - ); - vars.initialLockupBalance = vars.balances[0]; - vars.initialSenderBalance = vars.balances[1]; - vars.initialRecipientBalance = vars.balances[2]; - - // Expect the relevant events to be emitted. - vm.expectEmit({ emitter: address(lockup) }); - vars.senderAmount = lockup.refundableAmountOf(vars.streamId); - vars.recipientAmount = lockup.withdrawableAmountOf(vars.streamId); - emit ISablierLockupBase.CancelLockupStream( - vars.streamId, params.sender, params.recipient, FORK_TOKEN, vars.senderAmount, vars.recipientAmount - ); - vm.expectEmit({ emitter: address(lockup) }); - emit IERC4906.MetadataUpdate({ _tokenId: vars.streamId }); - - // Cancel the stream. - resetPrank({ msgSender: params.sender }); - uint128 refundedAmount = lockup.cancel(vars.streamId); - - // Assert that the refunded amount is correct. - assertEq(refundedAmount, vars.senderAmount, "refundedAmount"); - - // Assert that the stream's status is correct. - vars.actualStatus = lockup.statusOf(vars.streamId); - vars.expectedStatus = vars.recipientAmount > 0 ? Lockup.Status.CANCELED : Lockup.Status.DEPLETED; - assertEq(vars.actualStatus, vars.expectedStatus, "post-cancel stream status"); - - // Load the post-cancel token balances. - vars.balances = getTokenBalances( - address(FORK_TOKEN), Solarray.addresses(address(lockup), params.sender, params.recipient) - ); - vars.actualLockupBalance = vars.balances[0]; - vars.actualSenderBalance = vars.balances[1]; - vars.actualRecipientBalance = vars.balances[2]; - - // Assert that the contract's balance has been updated. - vars.expectedLockupBalance = vars.initialLockupBalance - uint256(vars.senderAmount); - assertEq(vars.actualLockupBalance, vars.expectedLockupBalance, "post-cancel lockup contract balance"); - - // Assert that the Sender's balance has been updated. - vars.expectedSenderBalance = vars.initialSenderBalance + uint256(vars.senderAmount); - assertEq(vars.actualSenderBalance, vars.expectedSenderBalance, "post-cancel Sender balance"); - - // Assert that the Recipient's balance has not changed. - vars.expectedRecipientBalance = vars.initialRecipientBalance; - assertEq(vars.actualRecipientBalance, vars.expectedRecipientBalance, "post-cancel Recipient balance"); - } - - // Assert that the not burned NFT. - vars.actualNFTOwner = lockup.ownerOf({ tokenId: vars.streamId }); - vars.expectedNFTOwner = params.recipient; - assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "post-cancel NFT owner"); + // Run the cancel test. + cancel(params); } } diff --git a/tests/integration/concrete/lockup-base/getters/getters.t.sol b/tests/integration/concrete/lockup-base/getters/getters.t.sol index 9d9ee9163..3b2c7d31f 100644 --- a/tests/integration/concrete/lockup-base/getters/getters.t.sol +++ b/tests/integration/concrete/lockup-base/getters/getters.t.sol @@ -169,7 +169,7 @@ contract Getters_Integration_Concrete_Test is Integration_Test { function test_GetUnderlyingTokenGivenNotNull() external view { IERC20 actualUnderlyingToken = lockup.getUnderlyingToken(ids.defaultStream); IERC20 expectedUnderlyingToken = dai; - assertEq(actualUnderlyingToken, expectedUnderlyingToken, "underlyingToken"); + assertEq(actualUnderlyingToken, expectedUnderlyingToken); } /*////////////////////////////////////////////////////////////////////////// diff --git a/tests/integration/concrete/lockup-dynamic/create-with-durations-ld/createWithDurationsLD.t.sol b/tests/integration/concrete/lockup-dynamic/create-with-durations-ld/createWithDurationsLD.t.sol index 8328f3cea..4edd7dcd3 100644 --- a/tests/integration/concrete/lockup-dynamic/create-with-durations-ld/createWithDurationsLD.t.sol +++ b/tests/integration/concrete/lockup-dynamic/create-with-durations-ld/createWithDurationsLD.t.sol @@ -173,7 +173,7 @@ contract CreateWithDurationsLD_Integration_Concrete_Test is Lockup_Dynamic_Integ assertEq(lockup.getRecipient(streamId), users.recipient, "recipient"); assertEq(lockup.getSender(streamId), users.sender, "sender"); assertEq(lockup.getStartTime(streamId), timestamps.start, "startTime"); - assertEq(lockup.getUnderlyingToken(streamId), dai, "underlyingToken"); + assertEq(lockup.getUnderlyingToken(streamId), dai); assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); // Assert that the stream's status is "STREAMING". diff --git a/tests/integration/concrete/lockup-dynamic/create-with-timestamps-ld/createWithTimestampsLD.t.sol b/tests/integration/concrete/lockup-dynamic/create-with-timestamps-ld/createWithTimestampsLD.t.sol index d13f700bc..5ad76c4db 100644 --- a/tests/integration/concrete/lockup-dynamic/create-with-timestamps-ld/createWithTimestampsLD.t.sol +++ b/tests/integration/concrete/lockup-dynamic/create-with-timestamps-ld/createWithTimestampsLD.t.sol @@ -299,7 +299,7 @@ contract CreateWithTimestampsLD_Integration_Concrete_Test is CreateWithTimestamp // It should create the stream. assertEqStream(streamId); - assertEq(lockup.getUnderlyingToken(streamId), IERC20(token), "underlyingToken"); + assertEq(lockup.getUnderlyingToken(streamId), IERC20(token)); assertEq(lockup.getSegments(streamId), defaults.segments()); assertEq(lockup.getLockupModel(streamId), Lockup.Model.LOCKUP_DYNAMIC); } diff --git a/tests/integration/concrete/lockup-linear/create-with-durations-ll/createWithDurationsLL.t.sol b/tests/integration/concrete/lockup-linear/create-with-durations-ll/createWithDurationsLL.t.sol index ed3bea568..44e15906d 100644 --- a/tests/integration/concrete/lockup-linear/create-with-durations-ll/createWithDurationsLL.t.sol +++ b/tests/integration/concrete/lockup-linear/create-with-durations-ll/createWithDurationsLL.t.sol @@ -73,9 +73,8 @@ contract CreateWithDurationsLL_Integration_Concrete_Test is Lockup_Linear_Integr assertEq(lockup.getRecipient(streamId), users.recipient, "recipient"); assertEq(lockup.getSender(streamId), users.sender, "sender"); assertEq(lockup.getStartTime(streamId), timestamps.start, "startTime"); - assertEq(lockup.getUnderlyingToken(streamId), dai, "underlyingToken"); - assertEq(lockup.getUnlockAmounts(streamId).start, _defaultParams.unlockAmounts.start, "unlockAmounts.start"); - assertEq(lockup.getUnlockAmounts(streamId).cliff, _defaultParams.unlockAmounts.cliff, "unlockAmounts.cliff"); + assertEq(lockup.getUnderlyingToken(streamId), dai); + assertEq(lockup.getUnlockAmounts(streamId), _defaultParams.unlockAmounts); assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); // Assert that the stream's status is "STREAMING". diff --git a/tests/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.t.sol b/tests/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.t.sol index ea0d76909..89e121e51 100644 --- a/tests/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.t.sol +++ b/tests/integration/concrete/lockup-linear/create-with-timestamps-ll/createWithTimestampsLL.t.sol @@ -224,8 +224,7 @@ contract CreateWithTimestampsLL_Integration_Concrete_Test is CreateWithTimestamp assertEqStream(streamId); assertEq(lockup.getCliffTime(streamId), cliffTime, "cliffTime"); assertEq(lockup.getLockupModel(streamId), Lockup.Model.LOCKUP_LINEAR); - assertEq(lockup.getUnderlyingToken(streamId), IERC20(token), "underlyingToken"); - assertEq(lockup.getUnlockAmounts(streamId).start, _defaultParams.unlockAmounts.start, "unlockAmounts.start"); - assertEq(lockup.getUnlockAmounts(streamId).cliff, _defaultParams.unlockAmounts.cliff, "unlockAmounts.cliff"); + assertEq(lockup.getUnderlyingToken(streamId), IERC20(token)); + assertEq(lockup.getUnlockAmounts(streamId), _defaultParams.unlockAmounts); } } diff --git a/tests/integration/concrete/lockup-linear/get-unlock-amounts/getUnlockAmounts.t.sol b/tests/integration/concrete/lockup-linear/get-unlock-amounts/getUnlockAmounts.t.sol index 7d7ca034f..30c6860cb 100644 --- a/tests/integration/concrete/lockup-linear/get-unlock-amounts/getUnlockAmounts.t.sol +++ b/tests/integration/concrete/lockup-linear/get-unlock-amounts/getUnlockAmounts.t.sol @@ -26,14 +26,12 @@ contract GetUnlockAmounts_Integration_Concrete_Test is Lockup_Linear_Integration _defaultParams.unlockAmounts = defaults.unlockAmountsZero(); uint256 streamId = createDefaultStream(); LockupLinear.UnlockAmounts memory unlockAmounts = lockup.getUnlockAmounts(streamId); - assertEq(unlockAmounts.start, 0, "unlockAmounts.start"); - assertEq(unlockAmounts.cliff, 0, "unlockAmounts.cliff"); + assertEq(unlockAmounts, _defaultParams.unlockAmounts); } function test_GivenStartUnlockAmountZero() external view givenNotNull givenLinearModel givenOnlyOneAmountZero { LockupLinear.UnlockAmounts memory unlockAmounts = lockup.getUnlockAmounts(ids.defaultStream); - assertEq(unlockAmounts.start, 0, "unlockAmounts.start"); - assertEq(unlockAmounts.cliff, defaults.CLIFF_AMOUNT(), "unlockAmounts.cliff"); + assertEq(unlockAmounts, _defaultParams.unlockAmounts); } function test_GivenStartUnlockAmountNotZero() external givenNotNull givenLinearModel givenOnlyOneAmountZero { @@ -41,15 +39,13 @@ contract GetUnlockAmounts_Integration_Concrete_Test is Lockup_Linear_Integration _defaultParams.unlockAmounts.cliff = 0; uint256 streamId = createDefaultStream(); LockupLinear.UnlockAmounts memory unlockAmounts = lockup.getUnlockAmounts(streamId); - assertEq(unlockAmounts.start, 1, "unlockAmounts.start"); - assertEq(unlockAmounts.cliff, 0, "unlockAmounts.cliff"); + assertEq(unlockAmounts, _defaultParams.unlockAmounts); } function test_GivenBothAmountsNotZero() external givenNotNull givenLinearModel { _defaultParams.unlockAmounts.start = 1; uint256 streamId = createDefaultStream(); LockupLinear.UnlockAmounts memory unlockAmounts = lockup.getUnlockAmounts(streamId); - assertEq(unlockAmounts.start, 1, "unlockAmounts.start"); - assertEq(unlockAmounts.cliff, defaults.CLIFF_AMOUNT(), "unlockAmounts.cliff"); + assertEq(unlockAmounts, _defaultParams.unlockAmounts); } } diff --git a/tests/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.t.sol b/tests/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.t.sol index 8fbe274c3..286393b12 100644 --- a/tests/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.t.sol +++ b/tests/integration/concrete/lockup-tranched/create-with-durations-lt/createWithDurationsLT.t.sol @@ -166,7 +166,7 @@ contract CreateWithDurationsLT_Integration_Concrete_Test is Lockup_Tranched_Inte assertEq(lockup.getSender(streamId), users.sender, "sender"); assertEq(lockup.getStartTime(streamId), timestamps.start, "startTime"); assertEq(lockup.getTranches(streamId), tranches); - assertEq(lockup.getUnderlyingToken(streamId), dai, "underlyingToken"); + assertEq(lockup.getUnderlyingToken(streamId), dai); assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); // Assert that the stream's status is "STREAMING". diff --git a/tests/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.t.sol b/tests/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.t.sol index 4b34d25ae..fe9664388 100644 --- a/tests/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.t.sol +++ b/tests/integration/concrete/lockup-tranched/create-with-timestamps-lt/createWithTimestampsLT.t.sol @@ -291,6 +291,6 @@ contract CreateWithTimestampsLT_Integration_Concrete_Test is CreateWithTimestamp assertEqStream(streamId); assertEq(lockup.getLockupModel(streamId), Lockup.Model.LOCKUP_TRANCHED); assertEq(lockup.getTranches(streamId), defaults.tranches()); - assertEq(lockup.getUnderlyingToken(streamId), IERC20(token), "underlyingToken"); + assertEq(lockup.getUnderlyingToken(streamId), IERC20(token)); } } diff --git a/tests/integration/fuzz/lockup-dynamic/createWithDurationsLD.t.sol b/tests/integration/fuzz/lockup-dynamic/createWithDurationsLD.t.sol index b8b2b015d..746009f61 100644 --- a/tests/integration/fuzz/lockup-dynamic/createWithDurationsLD.t.sol +++ b/tests/integration/fuzz/lockup-dynamic/createWithDurationsLD.t.sol @@ -83,7 +83,7 @@ contract CreateWithDurationsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integrati assertEq(lockup.getSender(streamId), users.sender, "sender"); assertEq(lockup.getSegments(streamId), vars.segmentsWithTimestamps); assertEq(lockup.getStartTime(streamId), timestamps.start, "startTime"); - assertEq(lockup.getUnderlyingToken(streamId), dai, "underlyingToken"); + assertEq(lockup.getUnderlyingToken(streamId), dai); assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); // Assert that the stream's status is correct. diff --git a/tests/integration/fuzz/lockup-dynamic/createWithTimestampsLD.t.sol b/tests/integration/fuzz/lockup-dynamic/createWithTimestampsLD.t.sol index 675f82035..cd4b5197c 100644 --- a/tests/integration/fuzz/lockup-dynamic/createWithTimestampsLD.t.sol +++ b/tests/integration/fuzz/lockup-dynamic/createWithTimestampsLD.t.sol @@ -200,17 +200,7 @@ contract CreateWithTimestampsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integrat vm.expectEmit({ emitter: address(lockup) }); emit ISablierLockup.CreateLockupDynamicStream({ streamId: expectedStreamId, - commonParams: Lockup.CreateEventCommon({ - funder: funder, - sender: params.sender, - recipient: params.recipient, - depositAmount: params.depositAmount, - token: dai, - cancelable: params.cancelable, - transferable: params.transferable, - timestamps: params.timestamps, - shape: params.shape - }), + commonParams: defaults.lockupCreateEvent(funder, params, dai), segments: segments }); @@ -233,7 +223,7 @@ contract CreateWithTimestampsLD_Integration_Fuzz_Test is Lockup_Dynamic_Integrat assertEq(lockup.getRecipient(streamId), params.recipient, "recipient"); assertEq(lockup.getSender(streamId), params.sender, "sender"); assertEq(lockup.getStartTime(streamId), params.timestamps.start, "startTime"); - assertEq(lockup.getUnderlyingToken(streamId), dai, "underlyingToken"); + assertEq(lockup.getUnderlyingToken(streamId), dai); assertEq(lockup.getSegments(streamId), segments); assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); diff --git a/tests/integration/fuzz/lockup-linear/createWithDurationsLL.t.sol b/tests/integration/fuzz/lockup-linear/createWithDurationsLL.t.sol index 3c2668503..2cfa4c37e 100644 --- a/tests/integration/fuzz/lockup-linear/createWithDurationsLL.t.sol +++ b/tests/integration/fuzz/lockup-linear/createWithDurationsLL.t.sol @@ -52,9 +52,8 @@ contract CreateWithDurationsLL_Integration_Fuzz_Test is Lockup_Linear_Integratio assertEq(lockup.getSender(streamId), users.sender, "sender"); assertEq(lockup.getStartTime(streamId), timestamps.start, "startTime"); assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); - assertEq(lockup.getUnderlyingToken(streamId), dai, "underlyingToken"); - assertEq(lockup.getUnlockAmounts(streamId).start, unlockAmounts.start, "unlockAmounts.start"); - assertEq(lockup.getUnlockAmounts(streamId).cliff, unlockAmounts.cliff, "unlockAmounts.cliff"); + assertEq(lockup.getUnderlyingToken(streamId), dai); + assertEq(lockup.getUnlockAmounts(streamId), unlockAmounts); // Assert that the stream's status is "STREAMING". Lockup.Status actualStatus = lockup.statusOf(streamId); diff --git a/tests/integration/fuzz/lockup-linear/createWithTimestampsLL.t.sol b/tests/integration/fuzz/lockup-linear/createWithTimestampsLL.t.sol index b5e6219db..27955a9d9 100644 --- a/tests/integration/fuzz/lockup-linear/createWithTimestampsLL.t.sol +++ b/tests/integration/fuzz/lockup-linear/createWithTimestampsLL.t.sol @@ -150,17 +150,7 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati vm.expectEmit({ emitter: address(lockup) }); emit ISablierLockup.CreateLockupLinearStream({ streamId: vars.expectedStreamId, - commonParams: Lockup.CreateEventCommon({ - funder: funder, - sender: params.sender, - recipient: params.recipient, - depositAmount: params.depositAmount, - token: dai, - cancelable: params.cancelable, - transferable: params.transferable, - timestamps: Lockup.Timestamps({ start: params.timestamps.start, end: params.timestamps.end }), - shape: params.shape - }), + commonParams: defaults.lockupCreateEvent(funder, params, dai), cliffTime: cliffTime, unlockAmounts: unlockAmounts }); @@ -182,9 +172,8 @@ contract CreateWithTimestampsLL_Integration_Fuzz_Test is Lockup_Linear_Integrati assertEq(lockup.getRecipient(vars.actualStreamId), params.recipient, "recipient"); assertEq(lockup.getSender(vars.actualStreamId), params.sender, "sender"); assertEq(lockup.getStartTime(vars.actualStreamId), params.timestamps.start, "startTime"); - assertEq(lockup.getUnderlyingToken(vars.actualStreamId), dai, "underlyingToken"); - assertEq(lockup.getUnlockAmounts(vars.actualStreamId).start, unlockAmounts.start, "unlockAmounts.start"); - assertEq(lockup.getUnlockAmounts(vars.actualStreamId).cliff, unlockAmounts.cliff, "unlockAmounts.cliff"); + assertEq(lockup.getUnderlyingToken(vars.actualStreamId), dai); + assertEq(lockup.getUnlockAmounts(vars.actualStreamId), unlockAmounts); assertFalse(lockup.wasCanceled(vars.actualStreamId), "wasCanceled"); // Assert that the stream's status is correct. diff --git a/tests/integration/fuzz/lockup-tranched/createWithDurationsLT.t.sol b/tests/integration/fuzz/lockup-tranched/createWithDurationsLT.t.sol index e5515c91f..bd1a4a77c 100644 --- a/tests/integration/fuzz/lockup-tranched/createWithDurationsLT.t.sol +++ b/tests/integration/fuzz/lockup-tranched/createWithDurationsLT.t.sol @@ -83,7 +83,7 @@ contract CreateWithDurationsLT_Integration_Fuzz_Test is Lockup_Tranched_Integrat assertEq(lockup.getSender(streamId), users.sender, "sender"); assertEq(lockup.getStartTime(streamId), timestamps.start, "startTime"); assertEq(lockup.getTranches(streamId), vars.tranchesWithTimestamps); - assertEq(lockup.getUnderlyingToken(streamId), dai, "underlyingToken"); + assertEq(lockup.getUnderlyingToken(streamId), dai); assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); // Assert that the stream's status is correct. diff --git a/tests/integration/fuzz/lockup-tranched/createWithTimestampsLT.t.sol b/tests/integration/fuzz/lockup-tranched/createWithTimestampsLT.t.sol index 045cfeb37..71cfb36a5 100644 --- a/tests/integration/fuzz/lockup-tranched/createWithTimestampsLT.t.sol +++ b/tests/integration/fuzz/lockup-tranched/createWithTimestampsLT.t.sol @@ -205,17 +205,7 @@ contract CreateWithTimestampsLT_Integration_Fuzz_Test is Lockup_Tranched_Integra vm.expectEmit({ emitter: address(lockup) }); emit ISablierLockup.CreateLockupTranchedStream({ streamId: expectedStreamId, - commonParams: Lockup.CreateEventCommon({ - funder: funder, - sender: params.sender, - recipient: params.recipient, - depositAmount: params.depositAmount, - token: dai, - cancelable: params.cancelable, - transferable: params.transferable, - timestamps: params.timestamps, - shape: params.shape - }), + commonParams: defaults.lockupCreateEvent(funder, params, dai), tranches: tranches }); @@ -242,7 +232,7 @@ contract CreateWithTimestampsLT_Integration_Fuzz_Test is Lockup_Tranched_Integra assertEq(lockup.getSender(streamId), params.sender, "sender"); assertEq(lockup.getStartTime(streamId), params.timestamps.start, "startTime"); assertEq(lockup.getTranches(streamId), tranches); - assertEq(lockup.getUnderlyingToken(streamId), dai, "underlyingToken"); + assertEq(lockup.getUnderlyingToken(streamId), dai); assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); // Assert that the stream's status is correct. diff --git a/tests/utils/Assertions.sol b/tests/utils/Assertions.sol index bdf80f1cf..8dfbea263 100644 --- a/tests/utils/Assertions.sol +++ b/tests/utils/Assertions.sol @@ -5,7 +5,8 @@ pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { PRBMathAssertions } from "@prb/math/test/utils/Assertions.sol"; -import { Lockup, LockupDynamic, LockupTranched } from "../../src/types/DataTypes.sol"; +import { ISablierLockup } from "../../src/interfaces/ISablierLockup.sol"; +import { Lockup, LockupDynamic, LockupLinear, LockupTranched } from "../../src/types/DataTypes.sol"; abstract contract Assertions is PRBMathAssertions { /*////////////////////////////////////////////////////////////////////////// @@ -32,14 +33,9 @@ abstract contract Assertions is PRBMathAssertions { assertEq(address(a), address(b)); } - /// @dev Compares two {IERC20} values. - function assertEq(IERC20 a, IERC20 b, string memory err) internal pure { - assertEq(address(a), address(b), err); - } - /// @dev Compares two {Lockup.Model} enum values. function assertEq(Lockup.Model a, Lockup.Model b) internal pure { - assertEq(uint8(a), uint8(b), "lockup model"); + assertEq(uint256(a), uint256(b), "Lockup.Model"); } /// @dev Compares two {Lockup.Timestamps} struct entities. @@ -58,6 +54,12 @@ abstract contract Assertions is PRBMathAssertions { } } + /// @dev Compares two {LockupLinear.UnlockAmounts} structs. + function assertEq(LockupLinear.UnlockAmounts memory a, LockupLinear.UnlockAmounts memory b) internal pure { + assertEq(a.start, b.start, "unlockAmounts.start"); + assertEq(a.cliff, b.cliff, "unlockAmounts.cliff"); + } + /// @dev Compares two {LockupTranched.Tranche} arrays. function assertEq(LockupTranched.Tranche[] memory a, LockupTranched.Tranche[] memory b) internal { if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { @@ -87,4 +89,28 @@ abstract contract Assertions is PRBMathAssertions { function assertNotEq(Lockup.Status a, Lockup.Status b, string memory err) internal pure { assertNotEq(uint256(a), uint256(b), err); } + + /// @dev Compares {SablierLockupBase} states with {Lockup.CreateWithTimestamps} parameters for a given stream ID. + function assertEq( + ISablierLockup lockup, + uint256 streamId, + Lockup.CreateWithTimestamps memory expectedLockup + ) + internal + view + { + assertEq(lockup.getDepositedAmount(streamId), expectedLockup.depositAmount, "depositedAmount"); + assertEq(lockup.getEndTime(streamId), expectedLockup.timestamps.end, "endTime"); + assertEq(lockup.getRecipient(streamId), expectedLockup.recipient, "recipient"); + assertEq(lockup.getSender(streamId), expectedLockup.sender, "sender"); + assertEq(lockup.getStartTime(streamId), expectedLockup.timestamps.start, "startTime"); + assertEq(lockup.getUnderlyingToken(streamId), expectedLockup.token); + assertEq(lockup.getWithdrawnAmount(streamId), 0, "withdrawnAmount"); + assertFalse(lockup.isDepleted(streamId), "isDepleted"); + assertTrue(lockup.isStream(streamId), "isStream"); + assertEq(lockup.isTransferable(streamId), expectedLockup.transferable, "isTransferable"); + assertEq(lockup.nextStreamId(), streamId + 1, "post-create nextStreamId"); + assertFalse(lockup.wasCanceled(streamId), "wasCanceled"); + assertEq(lockup.ownerOf(streamId), expectedLockup.recipient, "post-create NFT owner"); + } } diff --git a/tests/utils/Calculations.sol b/tests/utils/Calculations.sol index 976d300cc..dd245d16c 100644 --- a/tests/utils/Calculations.sol +++ b/tests/utils/Calculations.sol @@ -23,6 +23,11 @@ abstract contract Calculations { returns (uint128) { uint40 blockTimestamp = uint40(block.timestamp); + + if (startTime >= blockTimestamp) { + return 0; + } + if (blockTimestamp >= segments[segments.length - 1].timestamp) { return depositAmount; } @@ -75,12 +80,12 @@ abstract contract Calculations { if (startTime >= blockTimestamp) { return 0; } - if (blockTimestamp >= endTime) { - return depositAmount; - } if (cliffTime > blockTimestamp) { return unlockAmounts.start; } + if (blockTimestamp >= endTime) { + return depositAmount; + } unchecked { UD60x18 unlockAmountsSum = ud(unlockAmounts.start).add(ud(unlockAmounts.cliff)); @@ -109,6 +114,10 @@ abstract contract Calculations { returns (uint128) { uint40 blockTimestamp = uint40(block.timestamp); + + if (tranches[0].timestamp > blockTimestamp) { + return 0; + } if (blockTimestamp >= tranches[tranches.length - 1].timestamp) { return depositAmount; } diff --git a/tests/utils/Defaults.sol b/tests/utils/Defaults.sol index 14ffb0431..1e8dbe873 100644 --- a/tests/utils/Defaults.sol +++ b/tests/utils/Defaults.sol @@ -114,17 +114,32 @@ contract Defaults is CommonConstants { public view returns (Lockup.CreateEventCommon memory) + { + Lockup.CreateWithTimestamps memory params = createWithTimestamps(); + params.depositAmount = depositAmount; + params.timestamps = timestamps; + return lockupCreateEvent(users.sender, params, token_); + } + + function lockupCreateEvent( + address funder, + Lockup.CreateWithTimestamps memory params, + IERC20 token_ + ) + public + pure + returns (Lockup.CreateEventCommon memory) { return Lockup.CreateEventCommon({ - funder: users.sender, - sender: users.sender, - recipient: users.recipient, - depositAmount: depositAmount, + funder: funder, + sender: params.sender, + recipient: params.recipient, + depositAmount: params.depositAmount, token: token_, - cancelable: true, - transferable: true, - timestamps: timestamps, - shape: SHAPE + cancelable: params.cancelable, + transferable: params.transferable, + timestamps: params.timestamps, + shape: params.shape }); } diff --git a/tests/utils/EstimateMaxCount.sol b/tests/utils/EstimateMaxCount.sol index 7a1acfddb..53a5b9cbe 100644 --- a/tests/utils/EstimateMaxCount.sol +++ b/tests/utils/EstimateMaxCount.sol @@ -6,6 +6,8 @@ import { console } from "forge-std/src/console.sol"; import { ERC20Mock } from "@sablier/evm-utils/tests/mocks/erc20/ERC20Mock.sol"; import { ISablierLockup } from "../../src/interfaces/ISablierLockup.sol"; +import { LockupNFTDescriptor } from "../../src/LockupNFTDescriptor.sol"; +import { SablierLockup } from "../../src/SablierLockup.sol"; import { Lockup, LockupDynamic } from "../../src/types/DataTypes.sol"; import { Defaults } from "./Defaults.sol"; import { DeployOptimized } from "./DeployOptimized.t.sol"; @@ -48,8 +50,12 @@ contract EstimateMaxCount is Defaults, DeployOptimized { users.sender = users.recipient = payable(makeAddr("sender")); setUsers(users); - // Deploy the optimized Lockup contract. - (, lockup,) = deployOptimizedProtocol({ initialAdmin: users.sender, maxCount: MAX_COUNT }); + // Deploy the Lockup contract. + if (!isTestOptimizedProfile()) { + lockup = new SablierLockup(users.admin, new LockupNFTDescriptor(), MAX_COUNT); + } else { + (, lockup,) = deployOptimizedProtocol({ initialAdmin: users.sender, maxCount: MAX_COUNT }); + } // Set up the caller. resetPrank(users.sender);